diff --git a/devices/baseDevice.js b/devices/baseDevice.js index caee3716..05475c93 100644 --- a/devices/baseDevice.js +++ b/devices/baseDevice.js @@ -22,14 +22,20 @@ const formats = require('../formatvalues.js'); const fs = require('fs'); const path = require('path'); const util = require('util'); -const COOK_SUPPORTED_UNIT = ["UNKNOWN_UNITS", "NO_UNITS", "CENTIMETERS", "CUPS", "DECILITERS", "FEET", "FLUID_OUNCES", "GALLONS", "GRAMS", "INCHES", "KILOGRAMS", "LITERS", "METERS", "MILLIGRAMS", "MILLILITERS", "MILLIMETERS", "OUNCES", "PINCH", "PINTS", "PORTION", "POUNDS", "QUARTS", "TABLESPOONS", "TEASPOONS"]; -const DISPENSE_SUPPORTED_UNIT = ["CENTIMETERS", "CUPS", "DECILITERS", "FLUID_OUNCES", "GALLONS", "GRAMS", "KILOGRAMS", "LITERS", "MILLIGRAMS", "MILLILITERS", "MILLIMETERS", "NO_UNITS", "OUNCES", "PINCH", "PINTS", "PORTION", "POUNDS", "QUARTS", "TABLESPOONS", "TEASPOONS"]; +const COOK_SUPPORTED_UNITS = ["UNKNOWN_UNITS", "NO_UNITS", "CENTIMETERS", "CUPS", "DECILITERS", "FEET", "FLUID_OUNCES", "GALLONS", "GRAMS", "INCHES", "KILOGRAMS", "LITERS", "METERS", "MILLIGRAMS", "MILLILITERS", "MILLIMETERS", "OUNCES", "PINCH", "PINTS", "PORTION", "POUNDS", "QUARTS", "TABLESPOONS", "TEASPOONS"]; +const DISPENSE_SUPPORTED_UNITS = ["CENTIMETERS", "CUPS", "DECILITERS", "FLUID_OUNCES", "GALLONS", "GRAMS", "KILOGRAMS", "LITERS", "MILLIGRAMS", "MILLILITERS", "MILLIMETERS", "NO_UNITS", "OUNCES", "PINCH", "PINTS", "PORTION", "POUNDS", "QUARTS", "TABLESPOONS", "TEASPOONS"]; +const ENERGY_STORAGE_UNITS = ['SECONDS', 'MILES', 'KILOMETERS', 'PERCENTAGE', 'KILOWATT_HOURS']; +const LANGUAGES = ["da", "nl", "en", "fr", "de", "hi", "id", "it", "ja", "ko", "no", "pt-BR", "es", "sv", "th", "zh-TW"]; const Formats = { BOOL: 1, INT: 2, FLOAT: 4, STRING: 8, + DATETIME: 16, + PRIMITIVE: 31, + OBJECT: 32, + ARRAY: 64, MANDATORY: 128, COPY_OBJECT: 256, }; @@ -104,6 +110,7 @@ class BaseDevice { }; this.topicOut = config.topic; this.passthru = config.passthru; + this.persistent_state = config.persistent_state || false; this.room_hint = config.room_hint; this.device_type = config.device_type; @@ -749,17 +756,6 @@ class BaseDevice { this.updateStateTypesForTraits(); - this.exclusive_states = { - color: { - temperatureK: ['spectrumRgb', 'spectrumHsv'], - spectrumRgb: ['temperatureK', 'spectrumHsv'], - spectrumHsv: ['temperatureK', 'spectrumRgb'], - }, - thermostatTemperatureSetpointLow: ['thermostatTemperatureSetpoint'], - thermostatTemperatureSetpointHigh: ['thermostatTemperatureSetpoint'], - thermostatTemperatureSetpoint: ['thermostatTemperatureSetpointLow', 'thermostatTemperatureSetpointHigh'], - }; - // GoogleSmartHomeNode -> (client.registerDevice -> DeviceNode.registerDevice), app.registerDevice this.states = this.clientConn.register(this, 'device', config.name); @@ -835,94 +831,185 @@ class BaseDevice { state_types['online'] = Formats.BOOL + Formats.MANDATORY; if (me.trait.appselector) { - state_types['currentApplication'] = Formats.STRING + Formats.MANDATORY; + let values = []; + me.available_applications.forEach(application => { + values.push(application.key); + }); + state_types['currentApplication'] = { + type: Formats.STRING + Formats.MANDATORY, + values: values + }; } if (me.trait.armdisarm) { state_types['isArmed'] = Formats.BOOL + Formats.MANDATORY; - state_types['currentArmLevel'] = Formats.STRING + Formats.MANDATORY; + let available = []; + me.available_arm_levels.forEach(function (al) { + available.push(al.level_name); + }); + state_types['currentArmLevel'] = { + type: Formats.STRING + Formats.MANDATORY, + available: available + }; state_types['exitAllowance'] = Formats.INT; } if (me.trait.brightness && !me.command_only_brightness) { - state_types['brightness'] = Formats.INT; + state_types['brightness'] = { + type: Formats.INT, + min: 0, + max: 100, + }; } if (me.trait.colorsetting) { if (!me.command_only_colorsetting) { if ((me.color_model === "rgb") || (me.color_model === 'rgb_temp')) { state_types['color'] = { - spectrumRgb: Formats.INT + Formats.MANDATORY + type: Formats.OBJECT, + attributes: { + spectrumRgb: { + type: Formats.INT + Formats.MANDATORY, + exclusiveStates: ['temperatureK', 'spectrumHsv'] + }, + } }; } else if ((me.color_model === "hsv") || (me.color_model === "hsv_temp")) { state_types['color'] = { - spectrumHsv: { - hue: Formats.FLOAT + Formats.MANDATORY, // float, representing hue as positive degrees in the range of [0.0, 360.0) - saturation: Formats.FLOAT + Formats.MANDATORY, // float, representing saturation as a percentage in the range [0.0, 1.0] - value: Formats.FLOAT + Formats.MANDATORY // float, representing value as a percentage in the range [0.0, 1.0] + type: Formats.OBJECT, + attributes: { + spectrumHsv: { + type: Formats.OBJECT, + attributes: { + hue: { + type: Formats.FLOAT + Formats.MANDATORY, // float, representing hue as positive degrees in the range of [0.0, 360.0) + min: 0.0, + max: 360.0, + }, + saturation: { + type: Formats.FLOAT + Formats.MANDATORY, // float, representing saturation as a percentage in the range [0.0, 1.0] + min: 0.0, + max: 1.0, + }, + value: { + type: Formats.FLOAT + Formats.MANDATORY, // float, representing value as a percentage in the range [0.0, 1.0] + min: 0.0, + max: 1.0, + }, + }, + exclusiveStates: ['temperatureK', 'spectrumRgb'] + }, } }; } else { state_types['color'] = {}; } if (me.color_model !== "rgb" && me.color_model !== "hsv") { - state_types.color.temperatureK = Formats.INT + Formats.MANDATORY; + state_types.color.attributes.temperatureK = { + type: { + type: Formats.INT + Formats.MANDATORY, + min: me.temperature_min_k, + max: me.temperature_max_k, + }, + exclusiveStates: ['spectrumRgb', 'spectrumHsv'] + } } } } if (me.trait.cook) { - state_types['currentCookingMode'] = Formats.STRING + Formats.MANDATORY; - state_types['currentFoodPreset'] = Formats.STRING; + let cooking_mode_values = ['NONE']; + cooking_mode_values.push(...me.supported_cooking_modes); + state_types['currentCookingMode'] = { + type: Formats.STRING + Formats.MANDATORY, + values: cooking_mode_values, + }; + let food_preset_values = ['NONE']; + food_preset_values.push(...me.food_presets.map(food_preset => food_preset.food_preset_name)); + state_types['currentFoodPreset'] = { + type: Formats.STRING + Formats.MANDATORY, + values: food_preset_values, + }; state_types['currentFoodQuantity'] = Formats.FLOAT; - state_types['currentFoodUnit'] = Formats.STRING; + state_types['currentFoodUnit'] = { + type: Formats.STRING, + values: COOK_SUPPORTED_UNITS, + }; } if (me.trait.dispense) { - state_types['dispenseItems'] = [ - { - itemName: Formats.STRING, + let dispense_items_values = []; + me.supported_dispense_items.forEach(function (item) { + dispense_items_values.push(item.item_name); + }); + state_types['dispenseItems'] = { + type: Formats.OBJECT + Formats.ARRAY, + attributes: { + itemName: { + type: Formats.STRING, + value: dispense_items_values, + }, amountRemaining: { - amount: Formats.FLOAT, - unit: Formats.STRING, + type: Formats.OBJECT, + attributes: { + amount: Formats.FLOAT, + unit: { + type: Formats.STRING, + values: DISPENSE_SUPPORTED_UNITS, + }, + } }, amountLastDispensed: { - amount: Formats.FLOAT, - unit: Formats.STRING, + type: Formats.OBJECT, + attributes: { + amount: Formats.FLOAT, + unit: { + type: Formats.STRING, + values: DISPENSE_SUPPORTED_UNITS, + }, + } }, isCurrentlyDispensing: Formats.BOOL, }, - { - keyId: 'itemName', - removeIfNoData: true, - } - ]; + keyId: 'itemName', + removeIfNoData: true, + }; } if (me.trait.dock) { state_types['isDocked'] = Formats.BOOL; } if (me.trait.energystorage) { state_types['descriptiveCapacityRemaining'] = Formats.STRING + Formats.MANDATORY; - state_types['capacityRemaining'] = [ - { + state_types['capacityRemaining'] = { + type: Formats.OBJECT + Formats.ARRAY, + attributes: { rawValue: Formats.INT + Formats.MANDATORY, - unit: Formats.STRING + Formats.MANDATORY, + unit: { + type: Formats.STRING + Formats.MANDATORY, + values: ENERGY_STORAGE_UNITS, + toUpperCase: true, + replaceAll: true, + }, }, - { - keyId: "unit", - addIfMissing: true, - removeIfNoData: true, - isValidKey: unit => ['SECONDS', 'MILES', 'KILOMETERS', 'PERCENTAGE', 'KILOWATT_HOURS'].includes(unit) - } - ]; + keyId: "unit", + addIfMissing: true, + removeIfNoData: true, + replaceAll: true, + isValidKey: unit => ENERGY_STORAGE_UNITS.includes(unit) + }; if (me.is_rechargeable) { - state_types['capacityUntilFull'] = [ - { + state_types['capacityUntilFull'] = { + type: Formats.OBJECT + Formats.ARRAY, + attributes: { rawValue: Formats.INT + Formats.MANDATORY, - unit: Formats.STRING + Formats.MANDATORY, + unit: { + type: Formats.STRING + Formats.MANDATORY, + values: ENERGY_STORAGE_UNITS, + toUpperCase: true, + replaceAll: true, + } }, - { - keyId: "unit", - addIfMissing: true, - removeIfNoData: true, - isValidKey: unit => ['SECONDS', 'MILES', 'KILOMETERS', 'PERCENTAGE', 'KILOWATT_HOURS'].includes(unit) - } - ]; + keyId: "unit", + addIfMissing: true, + removeIfNoData: true, + replaceAll: true, + isValidKey: unit => ENERGY_STORAGE_UNITS.includes(unit) + }; state_types['isCharging'] = Formats.BOOL; } state_types['isPluggedIn'] = Formats.BOOL; @@ -935,16 +1022,30 @@ class BaseDevice { state_types['currentFanSpeedPercent'] = Formats.INT; } if (me.available_fan_speeds.length > 0) { - state_types['currentFanSpeedSetting'] = Formats.STRING; + let values = []; + this.available_fan_speeds.forEach(function (fanspeed) { + values.push(fanspeed.speed_name); + }); + state_types['currentFanSpeedSetting'] = { + type: Formats.STRING, + values: values, + }; } } } if (me.trait.fill) { + let fill_level_values = me.available_fill_levels.map(fl => fl.level_name); state_types['isFilled'] = Formats.BOOL + Formats.MANDATORY; if (me.available_fill_levels.length > 0) { - state_types['currentFillLevel'] = Formats.STRING + Formats.MANDATORY; + state_types['currentFillLevel'] = { + type: Formats.STRING + Formats.MANDATORY, + values: fill_level_values, + }; } else { - state_types['currentFillLevel'] = Formats.STRING; + state_types['currentFillLevel'] = { + type: Formats.STRING, + values: fill_level_values, + }; } if (me.supports_fill_percent) { state_types['currentFillPercent'] = Formats.FLOAT + Formats.MANDATORY; @@ -960,11 +1061,23 @@ class BaseDevice { } if (me.trait.inputselector) { if (!me.command_only_input_selector) { - state_types['currentInput'] = Formats.STRING + Formats.MANDATORY; + let values = []; + me.available_inputs.forEach(function (input) { + values.push(input.key); + }); + state_types['currentInput'] = { + type: Formats.STRING + Formats.MANDATORY, + values: values, + }; } } if (me.trait.lighteffects) { - state_types['activeLightEffect'] = Formats.STRING + Formats.MANDATORY; + let light_effect_value = ['']; + light_effect_value.push(...me.supported_effects); + state_types['activeLightEffect'] = { + type: Formats.STRING + Formats.MANDATORY, + values: light_effect_value, + }; state_types['lightEffectEndUnixTimestampSec'] = Formats.INT; } // Locator @@ -973,38 +1086,76 @@ class BaseDevice { state_types['isJammed'] = Formats.BOOL; } if (me.trait.mediastate) { - // INACTIVE STANDBY ACTIVE - state_types['activityState'] = Formats.STRING; - // PAUSED PLAYING FAST_FORWARDING REWINDING BUFFERING STOPPED - state_types['playbackState'] = Formats.STRING; + state_types['activityState'] = { + type: Formats.STRING, + values: ["INACTIVE", "STANDBY", "ACTIVE"], + upperCase: true, + }; + state_types['playbackState'] = { + type: Formats.STRING, + values: ["PAUSED", "PLAYING", "FAST_FORWARDING", "REWINDING", "BUFFERING", "STOPPED"], + upperCase: true, + }; } if (me.trait.modes) { if (!me.command_only_modes) { - state_types['currentModeSettings'] = Formats.COPY_OBJECT + Formats.STRING; // Mode keys are not defined in advance, See the docs + let attributes = {}; + me.available_modes.forEach(function (mode) { + let values = []; + mode.settings.forEach(function (setting) { + values.push(setting.setting_name); + }); + attributes[mode.name] = { + type: Formats.STRING + Formats.MANDATORY, + values: values, + }; + }); + state_types['currentModeSettings'] = { + type: Formats.OBJECT, + attributes: attributes, + } } } if (me.trait.networkcontrol) { state_types['networkEnabled'] = Formats.BOOL; state_types['networkSettings'] = { - ssid: Formats.STRING + Formats.MANDATORY + type: Formats.OBJECT, + attributes: { + ssid: Formats.STRING + Formats.MANDATORY + } }; state_types['guestNetworkEnabled'] = Formats.BOOL; state_types['guestNetworkSettings'] = { - ssid: Formats.STRING + Formats.MANDATORY + type: Formats.OBJECT, + attributes: { + ssid: Formats.STRING + Formats.MANDATORY + } }; state_types['numConnectedDevices'] = Formats.INT; state_types['networkUsageMB'] = Formats.FLOAT; state_types['networkUsageLimitMB'] = Formats.FLOAT; state_types['networkUsageUnlimited'] = Formats.BOOL; state_types['lastNetworkDownloadSpeedTest'] = { - downloadSpeedMbps: Formats.FLOAT, - unixTimestampSec: Formats.INT, - status: Formats.STRING + type: Formats.OBJECT, + attributes: { + downloadSpeedMbps: Formats.FLOAT, + unixTimestampSec: Formats.INT, + status: { + type: Formats.STRING, + values: ['SUCCESS', 'FAILURE'], + }, + } }; state_types['lastNetworkUploadSpeedTest'] = { - uploadSpeedMbps: Formats.FLOAT, - unixTimestampSec: Formats.INT, - status: Formats.STRING + type: Formats.OBJECT, + attributes: { + uploadSpeedMbps: Formats.FLOAT, + unixTimestampSec: Formats.INT, + status: { + type: Formats.STRING, + values: ['SUCCESS', 'FAILURE'], + }, + } }; state_types['networkSpeedTestInProgress'] = Formats.BOOL; } @@ -1019,18 +1170,21 @@ class BaseDevice { if (me.open_direction.length < 2) { state_types['openPercent'] = Formats.FLOAT + Formats.MANDATORY; } else { - state_types['openState'] = [ - { + state_types['openState'] = { + type: Formats.OBJECT + Formats.ARRAY, + attributes: { openPercent: Formats.FLOAT + Formats.MANDATORY, - openDirection: Formats.STRING + Formats.MANDATORY + openDirection: { + type: Formats.STRING + Formats.MANDATORY, + values: me.open_direction, + upperCase: true, + }, }, - { - keyId: 'openDirection', - removeIfNoData: true, - replaceAll: false, - isValidKey: direction => me.open_direction.includes(direction.trim()) ? direction.trim() : undefined - } - ]; + keyId: 'openDirection', + removeIfNoData: true, + replaceAll: false, + isValidKey: direction => me.open_direction.includes(direction.trim()) ? direction.trim() : undefined + }; } } } @@ -1046,38 +1200,43 @@ class BaseDevice { } } if (me.trait.runcycle) { - state_types['currentRunCycle'] = [ - { + state_types['currentRunCycle'] = { + type: Formats.OBJECT + Formats.ARRAY, + attributes: { currentCycle: Formats.STRING + Formats.MANDATORY, nextCycle: Formats.STRING, - lang: Formats.STRING + Formats.MANDATORY + lang: { + type: Formats.STRING + Formats.MANDATORY, + defaultValue: me.lang, + values: LANGUAGES, + } }, - { - keyId: 'currentCycle', - addIfMissing: true, - removeIfNoData: true, - isValidKey: cycle => cycle.trim().length > 0 ? cycle.trim() : undefined - } - ]; + keyId: 'lang', + addIfMissing: true, + removeIfNoData: true, + replaceAll: true, + }; state_types['currentTotalRemainingTime'] = Formats.INT + Formats.MANDATORY; state_types['currentCycleRemainingTime'] = Formats.INT + Formats.MANDATORY; } // Scene if (me.trait.sensorstate) { - state_types['currentSensorStateData'] = [ - { - name: Formats.STRING + Formats.MANDATORY, + state_types['currentSensorStateData'] = { + type: Formats.OBJECT + Formats.ARRAY, + attributes: { + name: { + type: Formats.STRING + Formats.MANDATORY, + values: me.sensor_states_supported, + }, currentSensorState: Formats.STRING, rawValue: Formats.FLOAT }, - { - keyId: 'name', - addIfMissing: true, - removeIfNoData: true, - replaceAll: false, - isValidKey: name => me.sensor_states_supported.includes(name.trim()) ? name.trim() : undefined - } - ]; + keyId: 'name', + addIfMissing: true, + removeIfNoData: true, + replaceAll: false, + isValidKey: name => me.sensor_states_supported.includes(name.trim()) ? name.trim() : undefined + }; } if (me.trait.softwareupdate) { state_types['lastSoftwareUpdateUnixTimestampSec'] = Formats.INT + Formats.MANDATORY; @@ -1085,30 +1244,30 @@ class BaseDevice { if (me.trait.startstop) { state_types['isRunning'] = Formats.BOOL + Formats.MANDATORY; state_types['isPaused'] = Formats.BOOL; - state_types['activeZones'] = [ - Formats.STRING, - { - addIfMissing: true, - removeIfNoData: true, - isValidKey: zone => me.available_zones.includes(zone.trim()) ? zone.trim() : undefined - } - ]; + state_types['activeZones'] = { + type: Formats.STRING + Formats.ARRAY, + values: me.available_zones, + addIfMissing: true, + removeIfNoData: true, + replaceAll: true, + isValidKey: zone => me.available_zones.includes(zone.trim()) ? zone.trim() : undefined + }; } if (me.trait.statusreport) { - state_types['currentStatusReport'] = [ - { + state_types['currentStatusReport'] = { + type: Formats.OBJECT + Formats.ARRAY, + attributes: { blocking: Formats.BOOL, deviceTarget: Formats.STRING, priority: Formats.INT, statusCode: Formats.STRING }, - { - keyId: ['deviceTarget', 'statusCode'], - addIfMissing: true, - removeIfNoData: true, - isValidKey: nodeId => Object.keys(me.clientConn.getProperties([nodeId])).length > 0 ? nodeId : me.clientConn.getIdFromName(nodeId) - } - ]; + keyId: ['deviceTarget', 'statusCode'], + addIfMissing: true, + removeIfNoData: true, + replaceAll: true, + isValidKey: nodeId => Object.keys(me.clientConn.getProperties([nodeId])).length > 0 ? nodeId : me.clientConn.getIdFromName(nodeId) + }; } if (me.trait.temperaturecontrol) { if (!me.command_only_temperaturecontrol) { @@ -1125,11 +1284,23 @@ class BaseDevice { state_types['activeThermostatMode'] = Formats.STRING; state_types['targetTempReachedEstimateUnixTimestampSec'] = Formats.INT; state_types['thermostatHumidityAmbient'] = Formats.FLOAT; - state_types['thermostatMode'] = Formats.STRING + Formats.MANDATORY; + state_types['thermostatMode'] = { + type: Formats.STRING + Formats.MANDATORY, + values: me.available_thermostat_modes, + }; state_types['thermostatTemperatureAmbient'] = Formats.FLOAT + Formats.MANDATORY; - state_types['thermostatTemperatureSetpoint'] = Formats.FLOAT + Formats.MANDATORY; // 0 One of - state_types['thermostatTemperatureSetpointHigh'] = Formats.FLOAT + Formats.MANDATORY; // 1 One of - state_types['thermostatTemperatureSetpointLow'] = Formats.FLOAT + Formats.MANDATORY; // 1 One of + state_types['thermostatTemperatureSetpoint'] = { // 0 One of + type: Formats.FLOAT + Formats.MANDATORY, + exclusiveStates: ['thermostatTemperatureSetpointLow', 'thermostatTemperatureSetpointHigh'], + }; + state_types['thermostatTemperatureSetpointHigh'] = { // 1 One of + type: Formats.FLOAT + Formats.MANDATORY, + exclusiveStates: ['thermostatTemperatureSetpoint'], + }; + state_types['thermostatTemperatureSetpointLow'] = { // 1 One of + type: Formats.FLOAT + Formats.MANDATORY, + exclusiveStates: ['thermostatTemperatureSetpoint'], + }; } } if (me.trait.timer) { @@ -1140,7 +1311,14 @@ class BaseDevice { } if (me.trait.toggles) { if (!me.command_only_toggles) { - state_types['currentToggleSettings'] = Formats.COPY_OBJECT + Formats.BOOL; // Toggles keys are not defined in advance, See the docs + let attributes = {}; + me.available_toggles.forEach(function (toggle) { + attributes[toggle.name] = Formats.BOOL + Formats.MANDATORY; + }); + state_types['currentToggleSettings'] = { + type: Formats.OBJECT, + attributes: attributes, + }; } } // TransportControl @@ -1454,16 +1632,16 @@ class BaseDevice { }); me.supported_dispense_presets.forEach(function (item) { dispense.push({ - "itemName": item.preset_name, - "amountRemaining": { - "amount": 0, - "unit": "NO_UNITS" + itemName: item.preset_name, + amountRemaining: { + amount: 0, + unit: "NO_UNITS" }, - "amountLastDispensed": { - "amount": 0, - "unit": "NO_UNITS" + amountLastDispensed: { + amount: 0, + unit: "NO_UNITS" }, - "isCurrentlyDispensing": false + isCurrentlyDispensing: false }) }); return dispense; @@ -1503,7 +1681,7 @@ class BaseDevice { } if (me.trait.cook) { states['currentCookingMode'] = "NONE"; - // states['currentFoodPreset'] = "NONE"; + states['currentFoodPreset'] = "NONE"; // states['currentFoodQuantity'] = 0; // states['currentFoodUnit'] = "UNKNOWN_UNITS"; } @@ -1710,7 +1888,7 @@ class BaseDevice { // states['activeThermostatMode'] = "none"; // states['targetTempReachedEstimateUnixTimestampSec'] = me.target_temp_reached_estimate_unix_timestamp_sec; // states['thermostatHumidityAmbient'] = me.thermostat_humidity_ambient; - states['thermostatMode'] = "none"; + states['thermostatMode'] = me.available_thermostat_modes.length > 0 ? me.available_thermostat_modes[0] : ""; states['thermostatTemperatureAmbient'] = me.thermostat_temperature_setpoint; // 0 states['thermostatTemperatureSetpoint'] = me.thermostat_temperature_setpoint; @@ -1811,21 +1989,25 @@ class BaseDevice { text = thermostat_mode.substr(0, 1).toUpperCase() + st; } else if (thermostat_mode === "heatcool") { fill = "green"; - text = "H/C T: " + (me.states.thermostatTemperatureAmbient || '?') + "°C | S: [" + (me.states.thermostatTemperatureSetpointLow || '') + " - " + (me.states.thermostatTemperatureSetpointHigh || '')+ "]\xB0C"; + text = "H/C T: " + (me.states.thermostatTemperatureAmbient || '?') + "°C | S: [" + (me.states.thermostatTemperatureSetpointLow || '') + " - " + (me.states.thermostatTemperatureSetpointHigh || '') + "]\xB0C"; } else { fill = "green"; text = thermostat_mode.substr(0, 1).toUpperCase() + st; } if (me.states.thermostatHumidityAmbient !== undefined) { - text += ' ' + me.states.thermostatHumidityAmbient + "% "; + text += ' ' + me.states.thermostatHumidityAmbient + "%"; } } if (me.trait.energystorage) { text += ' ' + me.states.descriptiveCapacityRemaining; } if (me.trait.armdisarm) { - if (me.states.currentArmLevel) { - text += ' ' + me.states.currentArmLevel; + if (me.states.isArmed) { + if (me.states.currentArmLevel) { + text += ' ' + me.states.currentArmLevel; + } + } else { + text += ' DISARMED'; } } } else { @@ -1843,7 +2025,7 @@ class BaseDevice { * */ updated(device, params, original_params) { - let me = this; + const me = this; let states = device.states; let command = device.command.startsWith('action.devices.commands.') ? device.command.substr(24) : device.command; this._debug(".updated: device.command = " + JSON.stringify(command)); @@ -1851,9 +2033,9 @@ class BaseDevice { this._debug(".updated: params = " + JSON.stringify(params)); this._debug(".updated: original_params = " + JSON.stringify(original_params)); - const modified = me.updateState(params || states); + const modified = me.updateState(params || states, me.states, me.state_types); if (modified) { - this.cloneObject(states, me.states, me.state_types, me.exclusive_states); + this.cloneObject(states, me.states, me.state_types); } this.updateStatusIcon(); @@ -2259,10 +2441,11 @@ class BaseDevice { // Update or Add reports based on deviceTarget and statusCode let payload = Array.isArray(msg.payload) ? msg.payload : [msg.payload]; let new_payload = []; + const status_report_type = me.state_types['currentStatusReport'].attributes; if (typeof me.states.currentStatusReport !== 'undefined') { me.states.currentStatusReport.forEach(report => { let new_report = { priority: 0 }; - me.cloneObject(new_report, report, me.state_types['currentStatusReport'][0]); + me.cloneObject(new_report, report, status_report_type); new_payload.push(new_report); }); } @@ -2281,12 +2464,12 @@ class BaseDevice { } if (nodeId) { let new_report = {}; - me.cloneObject(new_report, sr, me.state_types['currentStatusReport'][0]); + me.cloneObject(new_report, sr, status_report_type); if (new_report.statusCode) { new_report.deviceTarget = nodeId; let cur_reports = new_payload.filter(report => report.deviceTarget === nodeId && report.statusCode === new_report.statusCode); if (cur_reports.length > 0) { - if (me.cloneObject(cur_reports[0], new_report, me.state_types['currentStatusReport'][0])) { + if (me.cloneObject(cur_reports[0], new_report, status_report_type)) { differs = true; } } else { @@ -2296,10 +2479,11 @@ class BaseDevice { } } }); - if (me.updateState({ currentStatusReport: new_payload }) || differs) { + if (me.updateState({ currentStatusReport: new_payload }, me.states, me.state_types) || differs) { me.clientConn.setState(me, me.states, true); // tell Google ... - - me.clientConn.app.ScheduleGetState(); + if (me.persistent_state) { + me.clientConn.app.ScheduleGetState(); + } // if (me.passthru) { // msg.payload = new_payload; // me.send(msg); @@ -2460,14 +2644,16 @@ class BaseDevice { } else { me._debug(".input: some other topic"); } - const differs = me.updateState(msg.payload); + const differs = me.updateState(msg.payload, me.states, me.state_types); if (differs) { - // if (!me.passthru) { - // me.send({ topic: me.topicOut, payload: me.states }); - // } + if (msg.stateOutput || false) { + me.send({ topic: me.topicOut, payload: me.states }); + } me.clientConn.setState(me, me.states, true); // tell Google ... - me.clientConn.app.ScheduleGetState(); + if (me.persistent_state) { + me.clientConn.app.ScheduleGetState(); + } me.updateStatusIcon(); } if (me.passthru) { @@ -2645,27 +2831,41 @@ class BaseDevice { return traits; } - updateState(new_states) { + // + // + // + // + updateState(new_states, current_state, state_types) { const me = this; let modified = []; - Object.keys(me.state_types).forEach(function (key) { + me._debug("CCHI updateState state_types " + JSON.stringify(state_types)); + me._debug('updateState current state ' + JSON.stringify(current_state)); + Object.keys(state_types).forEach(key => { if (new_states.hasOwnProperty(key)) { - if (me.setState(key, new_states[key], me.states, me.state_types[key], me.exclusive_states[key] || {})) { - me._debug('.updateState: set "' + key + '" to ' + JSON.stringify(new_states[key])); - modified.push(key); + // console.log("CCHI found key " + key); + let o_modified = me.setState(key, new_states[key], current_state, state_types[key]); + if (o_modified) { + me._debug('.updateState set "' + key + '" to ' + JSON.stringify(new_states[key])); + modified.push(o_modified); } + // console.log("CCHI set " + key + " val " + JSON.stringify(current_state[key])); } + // else console.log("CCHI NOT found key " + key); }); + let thermostat_modified = false; if (modified.includes("thermostatTemperatureSetpoint")) { me.thermostat_temperature_setpoint = me.states.thermostatTemperatureSetpoint; + thermostat_modified = true; } if (modified.includes("thermostatTemperatureSetpointLow")) { me.thermostat_temperature_setpoint_low = me.states.thermostatTemperatureSetpointLow; + thermostat_modified = true; } if (modified.includes("thermostatTemperatureSetpointHigh")) { me.thermostat_temperature_setpoint_hight = me.states.thermostatTemperatureSetpointHigh; + thermostat_modified = true; } - if (modified.includes("thermostatMode")) { + if (thermostat_modified | modified.includes("thermostatMode")) { let keys_to_update = []; if (me.states.thermostatMode === 'heatcool') { keys_to_update = ['thermostatTemperatureSetpointLow', 'thermostatTemperatureSetpointHigh']; @@ -2680,259 +2880,354 @@ class BaseDevice { }; } keys_to_update.forEach(key => { - if (me.setState(key, new_states[key], me.states, me.state_types[key], me.exclusive_states[key] || {})) { + if (me.setState(key, new_states[key], me.states, me.state_types[key])) { me._debug('.updateState: set "' + key + '" to ' + JSON.stringify(new_states[key])); modified.push(key); } }); } - me._debug('.updateState: new State ' + modified + ' ' + JSON.stringify(me.states)); + me._debug('.updateState: new State ' + JSON.stringify(modified) + ' ' + JSON.stringify(me.states)); return modified; } - cloneObject(cur_obj, new_obj, state_values, exclusive_states) { + // + // + // + // + cloneObject(cur_obj, new_obj, state_values) { const me = this; let differs = false; - if (exclusive_states === undefined) { - exclusive_states = {}; - } Object.keys(state_values).forEach(function (key) { - if (typeof new_obj[key] !== 'undefined' && new_obj[key] != null) { - if (me.setState(key, new_obj[key], cur_obj, state_values[key] || {}, exclusive_states[key] || {})) { + const dvd = typeof state_values[key].defaultValue !== 'undefined'; + const new_value = typeof new_obj[key] !== 'undefined' ? new_obj[key] : state_values[key].defaultValue; + if ((typeof new_obj[key] !== 'undefined' && new_obj[key] != null) || dvd) { + if (me.setState(key, new_value, cur_obj, state_values[key] || {})) { differs = true; } - } else if (typeof state_values[key] === 'number' && !(state_values[key] & formats.MANDATORY)) { + } else if (!(state_values[key].type & Formats.MANDATORY)) { delete cur_obj[key]; } }); return differs; } - formatValue(key, value, type) { - let new_state; - if (type & Formats.FLOAT) { - new_state = formats.FormatValue(formats.Formats.FLOAT, key, value); - } else if (type & Formats.INT) { - new_state = formats.FormatValue(formats.Formats.INT, key, value); - } else if (type & Formats.STRING) { - new_state = formats.FormatValue(formats.Formats.STRING, key, value); - } else if (type & Formats.BOOL) { - new_state = formats.FormatValue(formats.Formats.BOOL, key, value); - } - return new_state; + // + // + // + // + formatValue(key, value, format, default_value) { + if (typeof value === 'undefined') { + value = default_value; + } + + if (typeof value === 'string') { + switch (format) { + case Formats.BOOL: + let t = value.toUpperCase() + + if (t == "TRUE" || t == "ON" || t == "YES" || t == "1") { + return true; + } else if (t == "FALSE" || t == "OFF" || t == "NO" || t == "0") { + return false; + } else { + throw new Error('Type of ' + key + ' is string but it cannot be converted to a boolean'); + } + + case Formats.STRING: + return value; + + case Formats.FLOAT: + let fval = parseFloat(value); + + if (isNaN(fval)) { + throw new Error('Type of ' + key + ' is string but it cannot be converted to a float'); + } + + return fval; + + case Formats.DATETIME: + return value; + + default: + let val = parseInt(value) + + if (isNaN(val)) { + throw new Error('Type of ' + key + ' is string but it cannot be converted to a integer'); + } + + return val; + } + } else if (typeof value === 'number') { + switch (format) { + case Formats.BOOL: + let val = (value != 0) + return val; + + case Formats.STRING: + return value.toString(); + + case Formats.DATETIME: + let dval = new Date(value); + return dval.toISOString(); + + default: + return value; + } + } else if (typeof value === 'boolean') { + switch (format) { + case Formats.BOOL: + return value; + + case Formats.STRING: + if (value) { + return "true"; + } else { + return "false"; + } + + default: + if (value) { + return 1; + } else { + return 0; + } + } + } else if (typeof value === 'object') { + if (value.hasOwnProperty(key)) { + return FormatValue(format, key, value[key]); + } else { + throw new Error('Type of ' + key + ' is object but it does not have matching property'); + } + } else { + throw new Error('Type of ' + key + ' is not compatible; typeof = ' + typeof value + "; value = " + JSON.stringify(value)); + } } - setState(key, value, states, state_values, exclusive_states) { + // + // + // + // + setState(key, value, state, state_type) { const me = this; let differs = false; - let old_state = typeof states === 'object' ? states[key] : {}; + let old_state = typeof state === 'object' ? state[key] : {}; let new_state = undefined; - let exclusive_states_arr = []; - if (Array.isArray(exclusive_states)) { - exclusive_states_arr = exclusive_states; - exclusive_states = {}; - } - if (typeof state_values === 'object') { - if (typeof value === "object") { - if (Array.isArray(state_values)) { - if (Array.isArray(value)) { - /* - if (JSON.stringify(states[key]) != JSON.stringify(value)) { - differs = true; + if (typeof state_type === 'number') { + state_type = { + type: state_type + }; + } + let exclusive_states = state_type.exclusiveStates || []; + // console.log("CCHI ---> setState key " + JSON.stringify(key) + " v " + JSON.stringify(value) + " ov " + JSON.stringify(old_state) + " st " + JSON.stringify(state_type) + " ex " + JSON.stringify(exclusive_states)); + + if (value == null) { + if (state_type.type & Formats.MANDATORY) { + me.RED.log.error("key " + key + " is mandatory."); + } else if (state.hasOwnProperty(key)) { + delete state[key]; + differs = key; + } + } else if (state_type.type & Formats.ARRAY) { + if (!Array.isArray(value)) { + value = [value]; + } + // checks array + if (!(state_type.type & Formats.OBJECT)) { + let new_arr = []; + let old_arr = Array.isArray(old_state) ? old_state : []; + const allowed_values = state_type.values; + value.forEach((elm, idx) => { + let new_val = me.formatValue(key + '[' + idx + ']', elm, state_type.type & Formats.PRIMITIVE); + if (state_type.upperCase && new_val) { + new_val = new_val.toUpperCase(); + } + if (new_val !== undefined && new_val !== null && (allowed_values === undefined || allowed_values.includes(new_val))) { + new_arr.push(new_val); + if (old_arr.length > idx) { + if (old_arr[idx] != new_val) { + differs = key; + } + } else { + differs = key; } - states[key] = value; - */ - // checks array - const ar_state_values = state_values[0]; - if (typeof ar_state_values === 'number') { - let new_arr = []; - let old_arr = Array.isArray(old_state) ? old_state : []; - value.forEach((elm, idx) => { - let new_val = me.formatValue(key + '[' + idx + ']', elm, ar_state_values); - if (new_val !== undefined && new_val != null) { - new_arr.push(new_val); - if (old_arr.length > idx) { - if (old_arr[idx] != new_val) { - differs = true; - } - } else { - differs = true; - } - } else { - differs = true; - } - }); - states[key] = new_arr; + } else { + differs = key; + } + }); + state[key] = new_arr; + } else { + // structure check + let new_arr = []; + let old_arr = Array.isArray(old_state) ? old_state : []; + let key_id = state_type.keyId || undefined; + let add_if_missing = typeof state_type.addIfMissing === 'boolean' ? state_type.addIfMissing : true; + let remove_if_no_data; + if (typeof state_type.removeIfNoData === 'boolean') { + remove_if_no_data = state_type.removeIfNoData; + } else { + remove_if_no_data = !(state_type.type & Formats.MANDATORY); + } + let is_valid_key; + let replace_all = state_type.replaceAll || key_id === undefined; + if (typeof state_type.isValidKey === 'function') { + is_valid_key = state_type.isValidKey; + } else { + is_valid_key = key => true; + } + value.forEach((new_obj, idx) => { + let cur_obj; + if (key_id) { + let f_arr; + let cloned_net_obj = {}; + me.cloneObject(cloned_net_obj, new_obj, state_type.attributes); + if (typeof key_id === 'string') { + f_arr = old_arr.filter(obj => { return obj[key_id] === cloned_net_obj[key_id] }); } else { - // structure check - let new_arr = []; - let old_arr = Array.isArray(old_state) ? old_state : []; - let key_id = state_values.length > 1 ? state_values[1] : undefined; - let add_if_missing = false; - let remove_if_no_data = false; - let is_valid_key = key => true; - let replace_all = false; - if (typeof key_id === 'object') { - add_if_missing = key_id.addIfMissing || false; - remove_if_no_data = key_id.removeIfNoData || false; - if (typeof key_id.isValidKey === 'function') { - is_valid_key = key_id.isValidKey; - } - if (typeof key_id.replaceAll === 'boolean') { - replace_all = key_id.replaceAll; - } else { - replace_all = true; - } - key_id = key_id.keyId; - } - value.forEach((new_obj, idx) => { - let cur_obj; - if (key_id) { - let f_arr; - if (typeof key_id === 'string') { - f_arr = old_arr.filter(obj => { return obj[key_id] === new_obj[key_id] }); - } else { - f_arr = old_arr.filter(obj => { - let obj_equal = true; - key_id.forEach(key_idi => { - if (obj[key_idi] !== new_obj[key_idi]) { - obj_equal = false; - } - }); - return obj_equal; - }); + f_arr = old_arr.filter(obj => { + let obj_equal = true; + key_id.forEach(key_idi => { + if (obj[key_idi] !== cloned_net_obj[key_idi]) { + obj_equal = false; } - if (f_arr.length > 1) { - this.RED.log.error('More than one "' + key + '" for "' + key_id + '" "' + new_obj[key_id] + '"'); - } else if (f_arr.length > 0) { - cur_obj = f_arr[0]; - } else if (add_if_missing) { - let key_id0 = typeof key_id === 'string' ? key_id : key_id[0]; - let key1 = is_valid_key(new_obj[key_id0]); - if (key1) { - cur_obj = {}; - if (typeof key1 === 'string') { - new_obj[key_id0] = key1; - } - old_arr.push(cur_obj); - } - } - } else { - cur_obj = old_arr[idx]; - if (cur_obj === undefined && add_if_missing) { - cur_obj = {}; - } - } - if (cur_obj !== undefined) { - if (me.cloneObject(cur_obj, new_obj, ar_state_values, exclusive_states)) { - differs = true; - } - if (Object.keys(cur_obj).length > 0) { - new_arr.push(cur_obj); - } else { - differs = true; // ?? - } - } + }); + return obj_equal; }); - if (replace_all && new_arr.length != old_arr.length) { - differs = true; - } - states[key] = replace_all ? new_arr : old_arr; - if (remove_if_no_data && states[key].length === 0) { - delete states[key]; + } + if (f_arr.length > 1) { + me.RED.log.error('More than one "' + key + '" for "' + key_id + '" "' + cloned_net_obj[key_id] + '"'); + } else if (f_arr.length > 0) { + cur_obj = f_arr[0]; + } else if (add_if_missing) { + let key_id0 = typeof key_id === 'string' ? key_id : key_id[0]; + let key1 = is_valid_key(cloned_net_obj[key_id0]); + if (key1) { + cur_obj = {}; + if (typeof key1 === 'string') { + new_obj[key_id0] = key1; + } + old_arr.push(cur_obj); } } } else { - this.RED.log.error('key "' + key + '" must be an array.'); + cur_obj = old_arr[idx]; + if (cur_obj === undefined && add_if_missing) { + cur_obj = {}; + } } - } else { - if (Array.isArray(value)) { - this.RED.log.error('key "' + key + '" must be an object.'); - } else { - if (states[key] === undefined) { - states[key] = {}; - old_state = states[key]; + if (cur_obj !== undefined) { + if (me.cloneObject(cur_obj, new_obj, state_type.attributes)) { + differs = key; + } + if (Object.keys(cur_obj).length > 0) { + new_arr.push(cur_obj); + } else { + differs = key; // ?? } - let mandatory_to_delete = []; - Object.keys(state_values).forEach(function (ikey) { - if (typeof value[ikey] !== 'undefined' && value[ikey] != null) { - if (typeof old_state[ikey] == 'undefined') { - old_state[ikey] = {}; - } - if (me.setState(ikey, value[ikey], old_state, state_values[ikey], exclusive_states[ikey] || {})) { - differs = true; - } - } else if (typeof state_values[ikey] === 'number' && !(state_values[ikey] & formats.MANDATORY)) { - if (typeof states[ikey] != 'undefined') { - differs = true; - } - delete states[ikey]; - } else { - mandatory_to_delete.push(ikey); - } - }); - mandatory_to_delete.forEach(ikey => { - const e_states = exclusive_states[ikey] || []; - let exclusive_state_found = false; - e_states.forEach(e_state => { - if (typeof states[e_state] !== 'undefined') { - exclusive_state_found = false; - } - }); - if (!exclusive_state_found) { - if (typeof states[ikey] != 'undefined') { - differs = true; - } - delete states[ikey]; - } else { - this.RED.log.error('key "' + key + '.' + ikey + '" is mandatory.'); - } - }); } + }); + if (replace_all && new_arr.length != old_arr.length) { + differs = key; } + state[key] = replace_all ? new_arr : old_arr; + if (remove_if_no_data && state[key].length === 0) { + delete state[key]; + } + } + } else if (state_type.type & Formats.OBJECT) { + if (Array.isArray(value)) { + me.RED.log.error('key "' + key + '" must be an object.'); } else { - if (Array.isArray(old_state)) { - this.RED.log.error('key "' + key + '" must be an array.'); - } else { - this.RED.log.error('key "' + key + '" must be an object.'); + if (state[key] === undefined) { + state[key] = {}; + old_state = state[key]; } + let mandatory_to_delete = []; + let o_differs = []; + Object.keys(state_type.attributes).forEach(function (ikey) { + // console.log("---> Attributes key " + ikey + " " + JSON.stringify(value[ikey])); + if (typeof value[ikey] !== 'undefined' && value[ikey] != null) { + if (typeof old_state[ikey] == 'undefined') { + old_state[ikey] = {}; + } + if (me.setState(ikey, value[ikey], old_state, state_type.attributes[ikey])) { + o_differs.push(ikey); + differs = o_differs; + } + } else { + const a_state_type = typeof state_type.attributes[ikey] === 'number' ? state_type.attributes[ikey] : state_type.attributes[ikey].type; + // console.log("a_state " + JSON.stringify(a_state_type)); + if (a_state_type & Formats.MANDATORY) { + mandatory_to_delete.push(ikey); + } else { + if (typeof state[ikey] != 'undefined') { + o_differs.push(ikey); + differs = o_differs; + } + delete state[key][ikey]; + // console.log("Deleted " + ikey + " " + JSON.stringify(state[key])); + } + } + }); + mandatory_to_delete.forEach(ikey => { + // console.log("try removing " + ikey); + let exclusive_state_found = false; + exclusive_states.forEach(e_state => { + if (typeof state[e_state] !== 'undefined') { + exclusive_state_found = false; + } + }); + if (!exclusive_state_found) { + if (typeof state[ikey] != 'undefined') { + o_differs.push(ikey); + differs = o_differs; + } + delete state[ikey]; + } else { + me.RED.log.error('key "' + key + '.' + ikey + '" is mandatory.'); + } + }); } - } else if (state_values & Formats.COPY_OBJECT) { + } else if (state_type.type & Formats.COPY_OBJECT) { if (typeof value !== 'object' || Array.isArray(value)) { - this.RED.log.error('key "' + key + '" must be an object.'); + me.RED.log.error('key "' + key + '" must be an object.'); } else { - Object.keys(old_state).forEach(function (key) { - if (typeof value[key] !== 'undefined') { - if (me.setState(key, value[key], old_state, state_values - Formats.COPY_OBJECT, {})) { - differs = true; + Object.keys(old_state).forEach(function (ikey) { + if (typeof value[ikey] !== 'undefined') { + if (me.setState(ikey, value[ikey], old_state, state_type.type - Formats.COPY_OBJECT)) { + differs = key; } } }); } - } else if (value == null) { - if (state_values & Formats.MANDATORY) { - this.RED.log.error("key " + key + " is mandatory."); - } else if (states.hasOwnProperty(key)) { - delete states[key]; - differs = true; - } - } else if (state_values & Formats.FLOAT) { - new_state = formats.FormatValue(formats.Formats.FLOAT, key, value); - } else if (state_values & Formats.INT) { - new_state = formats.FormatValue(formats.Formats.INT, key, value); - } else if (state_values & Formats.STRING) { - new_state = formats.FormatValue(formats.Formats.STRING, key, value); - } else if (state_values & Formats.BOOL) { - new_state = formats.FormatValue(formats.Formats.BOOL, key, value); - } - if (typeof state_values !== 'object') { - if (new_state !== undefined) { - differs = old_state !== new_state; - states[key] = new_state; - } - } - if (differs) { - exclusive_states_arr.forEach(rkey => delete states[rkey]); + } else { + new_state = me.formatValue(key, value, state_type.type & Formats.PRIMITIVE, state_type.defaultValue); + // console.log("CCHI checking new_state " + key + " " + new_state + " type " + JSON.stringify(state_type)); + if (state_type.min !== undefined && new_state < state_type.min) { + me.RED.log.error('key "' + key + '" must be greather or equal than ' + state_type.min); + new_state = undefined; + } else if (state_type.max !== undefined && new_state > state_type.max) { + me.RED.log.error('key "' + key + '" must be lower or equal than ' + state_type.max); + new_state = undefined; + } else if (Array.isArray(state_type.values) && !state_type.values.includes(new_state)) { + me.RED.log.error('key "' + key + '" must be one of ' + JSON.stringify(state_type.values)); + new_state = undefined; + } + } + if (new_state !== undefined && !(state_type.type & (Formats.OBJECT | Formats.ARRAY))) { + // console.log("CCHI Update state for " + key + " to " + new_state); + if (old_state !== new_state) { + differs = key; + } + state[key] = new_state; + } + if (exclusive_states.length > 0) { + exclusive_states.forEach(rkey => delete state[rkey]); + } + // console.log("CCHI END ----> " + key + " = " + JSON.stringify(state[key])); + if (Array.isArray(differs)) { + let o_differs = {}; + o_differs[key] = differs; + return o_differs; } return differs; } @@ -2960,7 +3255,7 @@ class BaseDevice { if (Array.isArray(data_in.supported_units)) { data_out.supported_units = []; data_in.supported_units.forEach(unit => { - if (typeof unit === 'string' && !data_out.supported_units.includes(unit.trim().toUpperCase()) && COOK_SUPPORTED_UNIT.includes(unit.trim().toUpperCase())) { + if (typeof unit === 'string' && !data_out.supported_units.includes(unit.trim().toUpperCase()) && COOK_SUPPORTED_UNITS.includes(unit.trim().toUpperCase())) { data_out.supported_units.push(unit.trim().toUpperCase()); } }); @@ -2976,7 +3271,7 @@ class BaseDevice { if (Array.isArray(data_in.supported_units)) { data_out.supported_units = []; data_in.supported_units.forEach(unit => { - if (typeof unit === 'string' && !data_out.supported_units.includes(unit.trim().toUpperCase()) && DISPENSE_SUPPORTED_UNIT.includes(unit.trim().toUpperCase())) { + if (typeof unit === 'string' && !data_out.supported_units.includes(unit.trim().toUpperCase()) && DISPENSE_SUPPORTED_UNITS.includes(unit.trim().toUpperCase())) { data_out.supported_units.push(unit.trim().toUpperCase()); } }); @@ -3078,6 +3373,16 @@ class BaseDevice { } } } + if (typeof rec[key1] == undefined && typeof rec[key2] !== undefined && Array.isArray(rec[key2])) { + let val2 = rec[key2]; + if (typeof val2 === 'string') { + val2 = [val2]; + } + let arr = val2.filter(element => typeof element === 'string' && element.trim().length > 0); + if (arr.length > 0 && arr[0]) { + rec[rec1] = arr[0]; + } + } if (typeof rec[key1] === 'string' && rec[key1].trim()) { let new_rec = {}; new_rec[key1] = rec[key1].trim(); @@ -4092,7 +4397,7 @@ class BaseDevice { params['thermostatTemperatureSetpointHigh'] = me.thermostat_temperature_setpoint_hight; params['thermostatTemperatureSetpointLow'] = me.thermostat_temperature_setpoint_low; executionStates.push('thermostatTemperatureSetpointHigh', 'thermostatTemperatureSetpointLow'); - } else if (thermostatMode === "heat" || thermostatMode === "cool") { + } else { params['thermostatTemperatureSetpoint'] = me.thermostat_temperature_setpoint; executionStates.push('thermostatTemperatureSetpoint'); } diff --git a/devices/device.html b/devices/device.html index b029b195..049a903d 100644 --- a/devices/device.html +++ b/devices/device.html @@ -62,6 +62,11 @@ + +
+ + +
@@ -2201,6 +2206,9 @@ passthru: { value: false, required: false }, + persistent_state: { + value: false, required: false + }, trait_scene: { value: false, required: false }, diff --git a/devices/locales/en-US/device.html b/devices/locales/en-US/device.html index 10100bcc..fd1384c2 100644 --- a/devices/locales/en-US/device.html +++ b/devices/locales/en-US/device.html @@ -162,11 +162,11 @@

Node Properties - Node-RED Settings

Pass Through If enabled, incoming messages will be passed onto the output. Be careful with this setting.

-

Language the spoken language used with the Google Smart Home.

+

Persistent state If enabled, the updated state will be included in the set_state message.

-

Show trait choose to show the selected, recommended or all traits.

+

Show trait Choose to show the selected, recommended or all traits.

-

Advanced settings shows the advanced settings. Don't modify them unless You know what you are doing.

+

Advanced settings Shows the advanced settings. Don't modify them unless You know what you are doing.

Input Message

If the msg.topic is one of the device states name the msg.payload must be the new value telling the new state of the device.

diff --git a/devices/locales/en-US/device.json b/devices/locales/en-US/device.json index 2db75498..ac3f1d79 100644 --- a/devices/locales/en-US/device.json +++ b/devices/locales/en-US/device.json @@ -231,7 +231,8 @@ "no_challenge": "No challenge", "ackNeeded": "Ack needed", "pinNeeded": "PIN needed", - "pin": "PIN" + "pin": "PIN", + "persistent_state": "Persistent state" }, "placeholder": { "name": "Name", diff --git a/devices/locales/it-IT/device.html b/devices/locales/it-IT/device.html index a7dabe4e..af32e595 100644 --- a/devices/locales/it-IT/device.html +++ b/devices/locales/it-IT/device.html @@ -162,11 +162,11 @@

Node Properties - Node-RED Settings

Pass Through If enabled, incoming messages will be passed onto the output. Be careful with this setting.

-

Language the spoken language used with the Google Smart Home.

+

Persistent state If enabled, the updated state will be included in the set_state message.

-

Show trait choose to show the selected, recommended or all traits.

+

Show trait Choose to show the selected, recommended or all traits.

-

Advanced settings shows the advanced settings. Don't modify them unless You know what you are doing.

+

Advanced settings Shows the advanced settings. Don't modify them unless You know what you are doing.

Input Message

If the msg.topic is one of the device states name the msg.payload must be the new value telling the new state of the device.

diff --git a/devices/locales/it-IT/device.json b/devices/locales/it-IT/device.json index 329db0c9..b538f4a2 100644 --- a/devices/locales/it-IT/device.json +++ b/devices/locales/it-IT/device.json @@ -250,7 +250,8 @@ "no_challenge": "Nessuna", "ackNeeded": "Si/No", "pinNeeded": "PIN", - "pin": "PIN" + "pin": "PIN", + "persistent_state": "Stato persistente" }, "placeholder": { "name": "Nome", diff --git a/google-smarthome.js b/google-smarthome.js index 592bec02..a4e01f89 100755 --- a/google-smarthome.js +++ b/google-smarthome.js @@ -269,33 +269,50 @@ module.exports = function(RED) { this.onInput = function (msg) { RED.log.debug("MgmtNode(input)"); - let topicArr = msg.topic.split(node.topicDelim); + let topicArr = (msg.topic || '').split(node.topicDelim); let topic = topicArr[topicArr.length - 1]; // get last part of topic + const topic_upper = topic.toUpperCase(); RED.log.debug("MgmtNode(input): topic = " + topic); try { - if (topic.toUpperCase() === 'RESTART_SERVER') { + if (topic_upper === 'RESTART_SERVER') { RED.log.debug("MgmtNode(input): RESTART_SERVER"); this.clientConn.app.Restart(RED.httpNode || RED.httpAdmin); - } else if (topic.toUpperCase() === 'REPORT_STATE') { + } else if (topic_upper === 'REPORT_STATE') { RED.log.debug("MgmtNode(input): REPORT_STATE"); this.clientConn.app.ReportAllStates(); - } else if (topic.toUpperCase() === 'REQUEST_SYNC') { + } else if (topic_upper === 'REQUEST_SYNC') { RED.log.debug("MgmtNode(input): REQUEST_SYNC"); this.clientConn.app.RequestSync(); - } else if (topic.toUpperCase() === 'GET_STATE') { - let states = this.clientConn.app.getStates(); + } else if (topic_upper === 'GET_STATE' || topic_upper === 'GETSTATE') { + let onlyPersistent = false; + let useNames = false; + let deviceIds = undefined; + if (typeof msg.payload === 'boolean') { + onlyPersistent = msg.payload; + } else if (typeof msg.payload === 'string') { + deviceIds = [ msg.payload ]; + } else if (typeof msg.payload === 'object') { + onlyPersistent = msg.payload.onlyPersistent || false; + useNames = msg.payload.useNames || false; + if (typeof msg.payload.devices === 'string') { + deviceIds = [ msg.payload.devices ]; + } else if (Array.isArray(msg.payload.devices)) { + deviceIds = msg.payload.devices; + } + } + let states = this.clientConn.app.getStates(deviceIds, onlyPersistent, useNames, RED); if (states) { this.send({ topic: topic, payload: states }); } - } else if (topic.toUpperCase() === 'SET_STATE') { + } else if (topic_upper === 'SET_STATE' || topic_upper === 'SETSTATE') { if (typeof msg.payload === 'object') { this.clientConn.app.setStates(msg.payload); } @@ -306,7 +323,7 @@ module.exports = function(RED) { }; this.sendSetState = function () { - let states = this.clientConn.app.getStates(); + let states = this.clientConn.app.getStates(undefined, true, false, RED); if (states) { this.send({ topic: 'set_state', diff --git a/lib/Devices.js b/lib/Devices.js index 68efb882..8ee2d5bd 100755 --- a/lib/Devices.js +++ b/lib/Devices.js @@ -265,7 +265,7 @@ class Devices { // // // - getStates(deviceIds) { + getStates(deviceIds, onlyPersistent, useNames, RED) { this.debug('Device:getStates(): deviceIds = ' + JSON.stringify(deviceIds)); if (!deviceIds || !Object.keys(deviceIds).length) { @@ -279,7 +279,18 @@ class Devices { if (!deviceIds) { Object.keys(me._devices).forEach(function (deviceId) { if (me._devices.hasOwnProperty(deviceId)) { - states[deviceId] = me._devices[deviceId].states; + let include = true; + let key = deviceId; + if (onlyPersistent === true || useNames === true) { + let node = RED.nodes.getNode(deviceId); + include = node.persistent_state; + if (useNames === true) { + key = node.name; + } + } + if (include) { + states[key] = me._devices[deviceId].states; + } } }); } else { diff --git a/locales/en-US/google-smarthome.html b/locales/en-US/google-smarthome.html index bf88ecd5..bc64e0f9 100644 --- a/locales/en-US/google-smarthome.html +++ b/locales/en-US/google-smarthome.html @@ -106,7 +106,7 @@

Input Messages

topic string
-
A topic of get_state requests to send to the output the state of all Google devices. msg.payload is not used for anything.
+
A topic of get_state requests to send to the output the state of all Google devices, if the msg.payload is not true, otherwise sends to the output the state of all Google devices with the "Persistent State" enabled.

If msg.topic is set_state the node updates the Google devices states with the states contained on the msg.payload.

diff --git a/locales/it_IT/google-smarthome.html b/locales/it_IT/google-smarthome.html index 1bbbf3ea..edd0b3b0 100644 --- a/locales/it_IT/google-smarthome.html +++ b/locales/it_IT/google-smarthome.html @@ -105,7 +105,7 @@

Messaggi in ingresso

topic stringa
-
Se un messaggio con topic get_state è ricevuto, il nodo invierà un messaggio in uscita con lo stato di tutti i dispositivi. msg.payload non è usato.
+
Se un messaggio con topic get_state è ricevuto, il nodo invierà un messaggio in uscita con lo stato di tutti i dispositivi, se msg.payload non è valorizzato a true, altrimenti il nodo invierà un messaggio in uscita con lo stato di tutti i dispositivi che hanno il campo "Stato persistente" abilitato.

Se il msg.topic è set_state, il nodo aggiorna lo stato di tutti i dispositivi con lo stato contenuto nel msg.payload. Questo messaggio può essere usato per salvare su un dispositivo di memorizzazione lo stato di tutti i dispositivi.

diff --git a/test/sh/device_test.sh b/test/sh/device_test.sh index 0941f4df..72736dc1 100644 --- a/test/sh/device_test.sh +++ b/test/sh/device_test.sh @@ -116,6 +116,8 @@ execute_payload() { echo "Request: " echo $REQUEST fi + while [ ! -f "$PAYLOAD_FILE" ] ; do sleep 1 ; done + sleep 1 PAYLOAD=$(cat "$PAYLOAD_FILE") REPORT_STATE=$(cat "$REPORT_STATE_FILE") LAST_NODE_ID="$NODE_ID" @@ -134,7 +136,8 @@ execute() { # echo "{}" > "$REPORT_STATE_FILE" ./execute "$@" > "$OUT_FILE" OUT=$(cat "$OUT_FILE") - #sleep 1 + while [ ! -f "$PAYLOAD_FILE" ] ; do sleep 1 ; done + sleep 1 PAYLOAD=$(cat "$PAYLOAD_FILE") REPORT_STATE=$(cat "$REPORT_STATE_FILE") LAST_NODE_ID="$1" @@ -236,6 +239,7 @@ test_sync .payload.devices[0].deviceInfo.swVersion "\""1.0"\"" test_sync .payload.devices[0].deviceInfo.hwVersion "\""1.0"\"" test_sync .payload.devices[0].id "\""$NODE_ID"\"" test_sync .payload.devices[1].id "\""$NODE_ID1"\"" +test_sync .payload.devices[2] null # availableApplications test_sync .payload.devices[0].attributes.availableApplications[0].key "\""YouTube"\"" @@ -726,9 +730,9 @@ test_sync .payload.devices[0].attributes.commandOnlyVolume false echo -execute_payload topic '{"online":false,"isArmed":false,"currentArmLevel":"","color":{"temperatureK":9000},"currentCookingMode":"NONE","dispenseItems":[{"itemName":"water","amountRemaining":{"amount":10,"unit":"NO_UNITS"},"amountLastDispensed":{"amount":11,"unit":"NO_UNITS"},"isCurrentlyDispensing":false},{"itemName":"cat_bowl","amountRemaining":{"amount":12,"unit":"NO_UNITS"},"amountLastDispensed":{"amount":13,"unit":"NO_UNITS"},"isCurrentlyDispensing":false},{"itemName":"glass_1","amountRemaining":{"amount":14,"unit":"NO_UNITS"},"amountLastDispensed":{"amount":15,"unit":"NO_UNITS"},"isCurrentlyDispensing":false}],"descriptiveCapacityRemaining":"FULL","capacityRemaining":[],"capacityUntilFull":[],"isPluggedIn":false,"currentFanSpeedPercent":0,"isFilled":false,"currentFillLevel":"","currentInput":"","activeLightEffect":"","currentModeSettings":{"load_mode":"","temp_mode":""},"openState":[{"openPercent":0,"openDirection":"UP"},{"openPercent":0,"openDirection":"DOWN"},{"openPercent":0,"openDirection":"LEFT"},{"openPercent":0,"openDirection":"RIGHT"},{"openPercent":0,"openDirection":"IN"},{"openPercent":0,"openDirection":"OUT"}],"currentRunCycle":[{"currentCycle":"unknown","lang":"en"}],"currentTotalRemainingTime":0,"currentCycleRemainingTime":0,"currentSensorStateData":[{"name":"AirQuality","currentSensorState":"unknown","rawValue":0},{"name":"CarbonMonoxideLevel","currentSensorState":"unknown","rawValue":0},{"name":"SmokeLevel","currentSensorState":"unknown","rawValue":0},{"name":"FilterCleanliness","currentSensorState":"unknown"},{"name":"WaterLeak","currentSensorState":"unknown"},{"name":"RainDetection","currentSensorState":"unknown"},{"name":"FilterLifeTime","currentSensorState":"unknown","rawValue":0},{"name":"PreFilterLifeTime","rawValue":0},{"name":"HEPAFilterLifeTime","rawValue":0},{"name":"Max2FilterLifeTime","rawValue":0},{"name":"CarbonDioxideLevel","rawValue":0},{"name":"PM2.5","rawValue":0},{"name":"PM10","rawValue":0},{"name":"VolatileOrganicCompounds","rawValue":0}],"lastSoftwareUpdateUnixTimestampSec":0,"isRunning":false,"currentStatusReport":[],"temperatureSetpointCelsius":0,"thermostatMode":"off","thermostatTemperatureAmbient":1,"thermostatTemperatureSetpoint":1,"timerRemainingSec":-1,"currentToggleSettings":{"quiet":false,"extra_bass":false,"Energy Saving":false},"currentVolume":40,"on":true,"isDocked":false}' +execute_payload fake_topic1 '{"online":false,"isArmed":false,"currentArmLevel":"","color":{"temperatureK":9000},"currentCookingMode":"NONE","dispenseItems":[{"itemName":"water","amountRemaining":{"amount":10,"unit":"NO_UNITS"},"amountLastDispensed":{"amount":11,"unit":"NO_UNITS"},"isCurrentlyDispensing":false},{"itemName":"cat_bowl","amountRemaining":{"amount":12,"unit":"NO_UNITS"},"amountLastDispensed":{"amount":13,"unit":"NO_UNITS"},"isCurrentlyDispensing":false},{"itemName":"glass_1","amountRemaining":{"amount":14,"unit":"NO_UNITS"},"amountLastDispensed":{"amount":15,"unit":"NO_UNITS"},"isCurrentlyDispensing":false}],"descriptiveCapacityRemaining":"FULL","capacityRemaining":[],"capacityUntilFull":[],"isPluggedIn":false,"currentFanSpeedPercent":0,"isFilled":false,"currentFillLevel":"half_level","currentInput":"hdmi 1","activeLightEffect":"colorLoop","currentModeSettings":{"load_mode":"large_load","temp_mode":"cold_temp"},"openState":[{"openPercent":0,"openDirection":"UP"},{"openPercent":0,"openDirection":"DOWN"},{"openPercent":0,"openDirection":"LEFT"},{"openPercent":0,"openDirection":"RIGHT"},{"openPercent":0,"openDirection":"IN"},{"openPercent":0,"openDirection":"OUT"}],"currentRunCycle":[{"currentCycle":"unknown","lang":"en"}],"currentTotalRemainingTime":0,"currentCycleRemainingTime":0,"currentSensorStateData":[{"name":"AirQuality","currentSensorState":"unknown","rawValue":0},{"name":"CarbonMonoxideLevel","currentSensorState":"unknown","rawValue":0},{"name":"SmokeLevel","currentSensorState":"unknown","rawValue":0},{"name":"FilterCleanliness","currentSensorState":"unknown"},{"name":"WaterLeak","currentSensorState":"unknown"},{"name":"RainDetection","currentSensorState":"unknown"},{"name":"FilterLifeTime","currentSensorState":"unknown","rawValue":0},{"name":"PreFilterLifeTime","rawValue":0},{"name":"HEPAFilterLifeTime","rawValue":0},{"name":"Max2FilterLifeTime","rawValue":0},{"name":"CarbonDioxideLevel","rawValue":0},{"name":"PM2.5","rawValue":0},{"name":"PM10","rawValue":0},{"name":"VolatileOrganicCompounds","rawValue":0}],"lastSoftwareUpdateUnixTimestampSec":0,"isRunning":false,"currentStatusReport":[],"temperatureSetpointCelsius":0,"thermostatMode":"off","thermostatTemperatureAmbient":1,"thermostatTemperatureSetpoint":1,"timerRemainingSec":-1,"currentToggleSettings":{"quiet":false,"extra_bass":false,"Energy Saving":false},"currentVolume":40,"on":true,"isDocked":false}' -execute_payload topic '{"online":true,"isArmed":false,"currentArmLevel":"","color":{"temperatureK":9000},"currentCookingMode":"NONE","dispenseItems":[{"itemName":"water","amountRemaining":{"amount":10,"unit":"NO_UNITS"},"amountLastDispensed":{"amount":11,"unit":"NO_UNITS"},"isCurrentlyDispensing":false},{"itemName":"cat_bowl","amountRemaining":{"amount":12,"unit":"NO_UNITS"},"amountLastDispensed":{"amount":13,"unit":"NO_UNITS"},"isCurrentlyDispensing":false},{"itemName":"glass_1","amountRemaining":{"amount":14,"unit":"NO_UNITS"},"amountLastDispensed":{"amount":15,"unit":"NO_UNITS"},"isCurrentlyDispensing":false}],"descriptiveCapacityRemaining":"FULL","capacityRemaining":[],"capacityUntilFull":[],"isPluggedIn":false,"currentFanSpeedPercent":0,"isFilled":false,"currentFillLevel":"","currentInput":"","activeLightEffect":"","currentModeSettings":{"load_mode":"","temp_mode":""},"openState":[{"openPercent":0,"openDirection":"UP"},{"openPercent":0,"openDirection":"DOWN"},{"openPercent":0,"openDirection":"LEFT"},{"openPercent":0,"openDirection":"RIGHT"},{"openPercent":0,"openDirection":"IN"},{"openPercent":0,"openDirection":"OUT"}],"currentRunCycle":[{"currentCycle":"unknown","lang":"en"}],"currentTotalRemainingTime":0,"currentCycleRemainingTime":0,"currentSensorStateData":[{"name":"AirQuality","currentSensorState":"unknown","rawValue":0},{"name":"CarbonMonoxideLevel","currentSensorState":"unknown","rawValue":0},{"name":"SmokeLevel","currentSensorState":"unknown","rawValue":0},{"name":"FilterCleanliness","currentSensorState":"unknown"},{"name":"WaterLeak","currentSensorState":"unknown"},{"name":"RainDetection","currentSensorState":"unknown"},{"name":"FilterLifeTime","currentSensorState":"unknown","rawValue":0},{"name":"PreFilterLifeTime","rawValue":0},{"name":"HEPAFilterLifeTime","rawValue":0},{"name":"Max2FilterLifeTime","rawValue":0},{"name":"CarbonDioxideLevel","rawValue":0},{"name":"PM2.5","rawValue":0},{"name":"PM10","rawValue":0},{"name":"VolatileOrganicCompounds","rawValue":0}],"lastSoftwareUpdateUnixTimestampSec":0,"isRunning":false,"currentStatusReport":[],"temperatureSetpointCelsius":0,"thermostatMode":"off","thermostatTemperatureAmbient":1,"thermostatTemperatureSetpoint":1,"timerRemainingSec":-1,"currentToggleSettings":{"quiet":false,"extra_bass":false,"Energy Saving":false},"currentVolume":40,"on":true,"isDocked":false}' +execute_payload fake_topic2 '{"online":true,"isArmed":false,"currentArmLevel":"","color":{"temperatureK":9000},"currentCookingMode":"NONE","dispenseItems":[{"itemName":"water","amountRemaining":{"amount":10,"unit":"NO_UNITS"},"amountLastDispensed":{"amount":11,"unit":"NO_UNITS"},"isCurrentlyDispensing":false},{"itemName":"cat_bowl","amountRemaining":{"amount":12,"unit":"NO_UNITS"},"amountLastDispensed":{"amount":13,"unit":"NO_UNITS"},"isCurrentlyDispensing":false},{"itemName":"glass_1","amountRemaining":{"amount":14,"unit":"NO_UNITS"},"amountLastDispensed":{"amount":15,"unit":"NO_UNITS"},"isCurrentlyDispensing":false}],"descriptiveCapacityRemaining":"FULL","capacityRemaining":[],"capacityUntilFull":[],"isPluggedIn":false,"currentFanSpeedPercent":0,"isFilled":false,"currentFillLevel":"full_level","currentInput":"hdmi 1","activeLightEffect":"sleep","currentModeSettings":{"load_mode":"small_load","temp_mode":"warm_temp"},"openState":[{"openPercent":0,"openDirection":"UP"},{"openPercent":0,"openDirection":"DOWN"},{"openPercent":0,"openDirection":"LEFT"},{"openPercent":0,"openDirection":"RIGHT"},{"openPercent":0,"openDirection":"IN"},{"openPercent":0,"openDirection":"OUT"}],"currentRunCycle":[{"currentCycle":"unknown","lang":"en"}],"currentTotalRemainingTime":0,"currentCycleRemainingTime":0,"currentSensorStateData":[{"name":"AirQuality","currentSensorState":"unknown","rawValue":0},{"name":"CarbonMonoxideLevel","currentSensorState":"unknown","rawValue":0},{"name":"SmokeLevel","currentSensorState":"unknown","rawValue":0},{"name":"FilterCleanliness","currentSensorState":"unknown"},{"name":"WaterLeak","currentSensorState":"unknown"},{"name":"RainDetection","currentSensorState":"unknown"},{"name":"FilterLifeTime","currentSensorState":"unknown","rawValue":0},{"name":"PreFilterLifeTime","rawValue":0},{"name":"HEPAFilterLifeTime","rawValue":0},{"name":"Max2FilterLifeTime","rawValue":0},{"name":"CarbonDioxideLevel","rawValue":0},{"name":"PM2.5","rawValue":0},{"name":"PM10","rawValue":0},{"name":"VolatileOrganicCompounds","rawValue":0}],"lastSoftwareUpdateUnixTimestampSec":0,"isRunning":false,"currentStatusReport":[],"temperatureSetpointCelsius":0,"thermostatMode":"off","thermostatTemperatureAmbient":1,"thermostatTemperatureSetpoint":1,"timerRemainingSec":-1,"currentToggleSettings":{"quiet":false,"extra_bass":false,"Energy Saving":false},"currentVolume":40,"on":true,"isDocked":false}' test_payload .online true test_payload .isArmed false test_payload .currentArmLevel '""' @@ -758,11 +762,11 @@ test_payload .capacityUntilFull null # '[]' test_payload .isPluggedIn false test_payload .currentFanSpeedPercent 0 test_payload .isFilled false -test_payload .currentFillLevel '""' -test_payload .currentInput '""' -test_payload .activeLightEffect '""' -test_payload .currentModeSettings.load_mode '""' -test_payload .currentModeSettings.temp_mode '""' +test_payload .currentFillLevel '"full_level"' +test_payload .currentInput '"hdmi 1"' +test_payload .activeLightEffect '"sleep"' +test_payload .currentModeSettings.load_mode '"small_load"' +test_payload .currentModeSettings.temp_mode '"warm_temp"' test_payload .openState[0].openDirection '"UP"' test_payload .openState[0].openPercent 0 test_payload .openState[1].openDirection '"DOWN"' @@ -840,15 +844,15 @@ test_payload .currentVolume 40 test_payload .on true test_payload .isDocked false -execute_payload topic '{"on":true, "isDocked":null}' +execute_payload fake_topic3 '{"on":true, "isDocked":null}' test_payload .isDocked null test_payload .on true -execute_payload topic '{"on":false, "isDocked":false}' +execute_payload fake_topic4 '{"on":false, "isDocked":false}' test_payload .isDocked false test_payload .on false -execute_payload topic '{"on":true, "isDocked":null}' +execute_payload fake_topic5 '{"on":true, "isDocked":null}' test_payload .on true test_payload .isDocked null @@ -870,12 +874,14 @@ test_payload .currentStatusReport[0].blocking false test_payload .currentStatusReport[0].priority 0 test_payload .currentStatusReport[0].statusCode '"allBattery"' test_payload .currentStatusReport[0].deviceTarget "\"$NODE_ID\"" +test_payload .currentStatusReport[1] null execute_payload currentStatusReport '[{"blocking":true,"deviceTarget":"MultiDevice Cucina","priority":1,"statusCode":"lowBattery"}]' test_payload .currentStatusReport[0].blocking true test_payload .currentStatusReport[0].priority 1 test_payload .currentStatusReport[0].statusCode '"lowBattery"' test_payload .currentStatusReport[0].deviceTarget "\"$NODE_ID\"" +test_payload .currentStatusReport[1] null execute_payload StatusReport '[{"blocking":false,"deviceTarget":"MultiDevice Cucina","priority":2,"statusCode":"okBattery"}]' test_payload .currentStatusReport[0].blocking true @@ -886,16 +892,18 @@ test_payload .currentStatusReport[1].blocking false test_payload .currentStatusReport[1].priority 2 test_payload .currentStatusReport[1].statusCode '"okBattery"' test_payload .currentStatusReport[1].deviceTarget "\"$NODE_ID\"" +test_payload .currentStatusReport[2] null -execute_payload StatusReport '[{"blocking":false,"deviceTarget":"MultiDevice Cucina","priority":2,"statusCode":"okBattery"}]' +execute_payload StatusReport '[{"blocking":false,"deviceTarget":"MultiDevice Cucina","priority":3,"statusCode":"okBattery"}]' test_payload .currentStatusReport[0].blocking true test_payload .currentStatusReport[0].priority 1 test_payload .currentStatusReport[0].statusCode '"lowBattery"' test_payload .currentStatusReport[0].deviceTarget "\"$NODE_ID\"" test_payload .currentStatusReport[1].blocking false -test_payload .currentStatusReport[1].priority 2 +test_payload .currentStatusReport[1].priority 3 test_payload .currentStatusReport[1].statusCode '"okBattery"' test_payload .currentStatusReport[1].deviceTarget "\"$NODE_ID\"" +test_payload .currentStatusReport[2] null execute_payload StatusReport '[]' test_payload .currentStatusReport[0].blocking true @@ -903,53 +911,73 @@ test_payload .currentStatusReport[0].priority 1 test_payload .currentStatusReport[0].statusCode '"lowBattery"' test_payload .currentStatusReport[0].deviceTarget "\"$NODE_ID\"" test_payload .currentStatusReport[1].blocking false -test_payload .currentStatusReport[1].priority 2 +test_payload .currentStatusReport[1].priority 3 test_payload .currentStatusReport[1].statusCode '"okBattery"' test_payload .currentStatusReport[1].deviceTarget "\"$NODE_ID\"" +test_payload .currentStatusReport[2] null -execute_payload StatusReport '[{"blocking":false,"deviceTarget":"MultiDevice Salotto","priority":3,"statusCode":"lowBattery"}]' +execute_payload StatusReport '[{"blocking":false,"deviceTarget":"MultiDevice Salotto","priority":4,"statusCode":"lowBattery"}]' test_payload .currentStatusReport[0].blocking true test_payload .currentStatusReport[0].priority 1 test_payload .currentStatusReport[0].statusCode '"lowBattery"' test_payload .currentStatusReport[0].deviceTarget "\"$NODE_ID\"" test_payload .currentStatusReport[1].blocking false -test_payload .currentStatusReport[1].priority 2 +test_payload .currentStatusReport[1].priority 3 test_payload .currentStatusReport[1].statusCode '"okBattery"' test_payload .currentStatusReport[1].deviceTarget "\"$NODE_ID\"" test_payload .currentStatusReport[2].blocking false -test_payload .currentStatusReport[2].priority 3 +test_payload .currentStatusReport[2].priority 4 test_payload .currentStatusReport[2].statusCode '"lowBattery"' test_payload .currentStatusReport[2].deviceTarget "\"$NODE_ID1\"" +test_payload .currentStatusReport[3] null execute_payload capacityRemaining '[{"unit":"SECONDS","rawValue":61}]' test_payload .capacityRemaining[0].unit '"SECONDS"' test_payload .capacityRemaining[0].rawValue 61 +test_payload .capacityRemaining[1] null execute_payload capacityUntilFull '[{"unit":"PERCENTAGE","rawValue":7}]' test_payload .capacityUntilFull[0].unit '"PERCENTAGE"' test_payload .capacityUntilFull[0].rawValue 7 +test_payload .capacityUntilFull[1] null execute_payload capacityRemaining '[{"unit":"SECONDS","rawValue":70},{"unit":"KILOMETERS","rawValue":27}]' test_payload .capacityRemaining[0].unit '"SECONDS"' test_payload .capacityRemaining[0].rawValue 70 test_payload .capacityRemaining[1].unit '"KILOMETERS"' test_payload .capacityRemaining[1].rawValue 27 +test_payload .capacityRemaining[2] null test_payload .capacityUntilFull[0].unit '"PERCENTAGE"' test_payload .capacityUntilFull[0].rawValue 7 +test_payload .capacityUntilFull[1] null execute_payload capacityUntilFull '[{"unit":"SECONDS","rawValue":60}]' test_payload .capacityUntilFull[0].unit '"SECONDS"' test_payload .capacityUntilFull[0].rawValue 60 +test_payload .capacityUntilFull[1] null test_payload .capacityRemaining[0].unit '"SECONDS"' test_payload .capacityRemaining[0].rawValue 70 test_payload .capacityRemaining[1].unit '"KILOMETERS"' test_payload .capacityRemaining[1].rawValue 27 +test_payload .capacityRemaining[2] null execute_payload capacityRemaining '[{"unit":"PERCENTAGE","rawValue":7}]' test_payload .capacityRemaining[0].unit '"PERCENTAGE"' test_payload .capacityRemaining[0].rawValue 7 +test_payload .capacityRemaining[1] null test_payload .capacityUntilFull[0].unit '"SECONDS"' test_payload .capacityUntilFull[0].rawValue 60 +test_payload .capacityUntilFull[1] null + +execute_payload capacityUntilFull '[{"unit":"SECONDS","rawValue":72},{"unit":"KILOMETERS","rawValue":29}]' +test_payload .capacityUntilFull[0].unit '"SECONDS"' +test_payload .capacityUntilFull[0].rawValue 72 +test_payload .capacityUntilFull[1].unit '"KILOMETERS"' +test_payload .capacityUntilFull[1].rawValue 29 +test_payload .capacityUntilFull[2] null +test_payload .capacityRemaining[0].unit '"PERCENTAGE"' +test_payload .capacityRemaining[0].rawValue 7 +test_payload .capacityRemaining[1] null execute_payload openState '[{"openDirection":"DOWN","openPercent":7}]' test_payload .openState[0].openDirection '"UP"' @@ -981,13 +1009,20 @@ test_payload .openState[5].openDirection '"OUT"' test_payload .openState[5].openPercent 0 test_payload .openState[6] null -execute_payload currentRunCycle '[{"currentCycle":"Riscaldamento piatto","nextCycle":"Riscaldamento ugello","lang":"it"},{"currentCycle":"Riscaldamento ugello","nextCycle":"Stampa","lang":"it"}]' +execute_payload currentRunCycle '[{"currentCycle":"Riscaldamento piatto","nextCycle":"Riscaldamento ugello","lang":"it"},{"currentCycle":"Nozzle heating","nextCycle":"Print","lang":"en"}]' test_payload .currentRunCycle[0].currentCycle '"Riscaldamento piatto"' test_payload .currentRunCycle[0].nextCycle '"Riscaldamento ugello"' test_payload .currentRunCycle[0].lang '"it"' -test_payload .currentRunCycle[1].currentCycle '"Riscaldamento ugello"' -test_payload .currentRunCycle[1].nextCycle '"Stampa"' -test_payload .currentRunCycle[1].lang '"it"' +test_payload .currentRunCycle[1].currentCycle '"Nozzle heating"' +test_payload .currentRunCycle[1].nextCycle '"Print"' +test_payload .currentRunCycle[1].lang '"en"' +test_payload .currentRunCycle[2] null + +execute_payload currentRunCycle '[{"currentCycle":"Stampa"}]' +test_payload .currentRunCycle[0].currentCycle '"Stampa"' +test_payload .currentRunCycle[0].nextCycle null +test_payload .currentRunCycle[0].lang '"it"' +test_payload .currentRunCycle[1] null execute_payload currentSensorStateData '[{"name":"CarbonMonoxideLevel","currentSensorState":"no carbon monoxide detected","rawValue":7}]' test_payload .currentSensorStateData[0].name '"AirQuality"' @@ -1046,7 +1081,10 @@ test_payload .activeZones[2] '"Soggiorno"' test_payload .activeZones[3] null execute_payload activeZones '["Bagno"]' -test_payload .activeZones[0] '"Bagno"' +test_payload .activeZones[0] null + +execute_payload activeZones '["Bagno", "Salotto", "Garage"]' +test_payload .activeZones[0] '"Salotto"' test_payload .activeZones[1] null execute_payload errorCode '"inSoftwareUpdate"' @@ -1105,11 +1143,19 @@ echo echo ArmDisarm The Arm/Disarm logic should be outside the node execute $NODE_ID ArmDisarm true 123 test_out ".payload.commands[0].states.online" true +test_payload .currentArmLevel '"L1"' +test_payload .isArmed true execute $NODE_ID ArmDisarm_level true L2 123 test_out ".payload.commands[0].states.online" true +test_payload .currentArmLevel '"L2"' +test_payload .isArmed true execute_error $NODE_ID ArmDisarm_level true NO_LEVEL 456 +test_out ".payload.commands[0].errorCode" '"transientError"' + +execute $NODE_ID ArmDisarm false +test_payload .isArmed false execute $NODE_ID ArmDisarm_cancel true test_out ".payload.commands[0].states.online" true @@ -1236,11 +1282,11 @@ test_payload ".color.spectrumHsv.saturation" null test_payload ".color.spectrumHsv.value" null test_out ".payload.commands[0].states.online" true -execute $NODE_ID1 ColorAbsolute_hsv 'Blu' 500 2 3 +execute $NODE_ID1 ColorAbsolute_hsv 'Blu' 360 0.5 0.5 test_out ".payload.commands[0].states.online" true -test_payload ".color.spectrumHsv.hue" 500 -test_payload ".color.spectrumHsv.saturation" 2 -test_payload ".color.spectrumHsv.value" 3 +test_payload ".color.spectrumHsv.hue" 360 +test_payload ".color.spectrumHsv.saturation" 0.5 +test_payload ".color.spectrumHsv.value" 0.5 test_payload ".color.temperatureK" null test_payload ".color.spectrumRgb" null @@ -1813,48 +1859,93 @@ test_payload ".temperatureSetpointCelsius" 16.5 # TemperatureSetting echo -echo TemperatureSetting +echo TemperatureSetting +execute $NODE_ID ThermostatSetMode "cool" +test_payload ".thermostatMode" '"cool"' + +execute $NODE_ID ThermostatSetMode "eco" +test_payload ".thermostatMode" '"eco"' +test_payload ".thermostatTemperatureSetpointHigh" null +test_payload ".thermostatTemperatureSetpointLow" null + +execute $NODE_ID ThermostatSetMode "heatcool" +test_payload ".thermostatMode" '"heatcool"' +test_payload ".thermostatTemperatureSetpoint" null + +execute $NODE_ID ThermostatSetMode "heat" +test_payload ".thermostatMode" '"heat"' +test_payload ".thermostatTemperatureSetpointHigh" null +test_payload ".thermostatTemperatureSetpointLow" null + execute $NODE_ID ThermostatTemperatureSetpoint 17.67 +test_payload ".thermostatMode" '"heat"' test_payload ".thermostatTemperatureSetpoint" 17.67 test_payload ".thermostatTemperatureSetpointHigh" null test_payload ".thermostatTemperatureSetpointLow" null execute $NODE_ID ThermostatTemperatureSetpoint 17.55 +test_payload ".thermostatMode" '"heat"' test_payload ".thermostatTemperatureSetpoint" 17.55 test_payload ".thermostatTemperatureSetpointHigh" null test_payload ".thermostatTemperatureSetpointLow" null execute $NODE_ID ThermostatTemperatureSetRange 26.2 22.8 +test_payload ".thermostatMode" '"heat"' +test_payload ".thermostatTemperatureSetpoint" 17.55 +test_payload ".thermostatTemperatureSetpointHigh" null +test_payload ".thermostatTemperatureSetpointLow" null + +execute $NODE_ID ThermostatSetMode "heatcool" +test_payload ".thermostatMode" '"heatcool"' test_payload ".thermostatTemperatureSetpoint" null test_payload ".thermostatTemperatureSetpointHigh" 26.2 test_payload ".thermostatTemperatureSetpointLow" 22.8 execute $NODE_ID ThermostatTemperatureSetRange 27.2 21.8 +test_payload ".thermostatMode" '"heatcool"' test_payload ".thermostatTemperatureSetpoint" null test_payload ".thermostatTemperatureSetpointHigh" 27.2 test_payload ".thermostatTemperatureSetpointLow" 21.8 execute $NODE_ID ThermostatTemperatureSetpoint 27.88 +test_payload ".thermostatMode" '"heatcool"' +test_payload ".thermostatTemperatureSetpoint" null +test_payload ".thermostatTemperatureSetpointHigh" 27.2 +test_payload ".thermostatTemperatureSetpointLow" 21.8 + +execute $NODE_ID ThermostatSetMode "cool" +test_payload ".thermostatMode" '"cool"' test_payload ".thermostatTemperatureSetpoint" 27.88 test_payload ".thermostatTemperatureSetpointHigh" null test_payload ".thermostatTemperatureSetpointLow" null +execute $NODE_ID ThermostatSetMode "heatcool" +test_payload ".thermostatMode" '"heatcool"' +test_payload ".thermostatTemperatureSetpoint" null +test_payload ".thermostatTemperatureSetpointHigh" 27.2 +test_payload ".thermostatTemperatureSetpointLow" 21.8 + execute $NODE_ID ThermostatTemperatureSetRange 16.2 12.8 +test_payload ".thermostatMode" '"heatcool"' test_payload ".thermostatTemperatureSetpoint" null test_payload ".thermostatTemperatureSetpointHigh" 16.2 test_payload ".thermostatTemperatureSetpointLow" 12.8 -execute $NODE_ID ThermostatSetMode "heatcool" +execute $NODE_ID ThermostatTemperatureSetpoint 15.67 test_payload ".thermostatMode" '"heatcool"' +test_payload ".thermostatTemperatureSetpoint" null +test_payload ".thermostatTemperatureSetpointHigh" 16.2 +test_payload ".thermostatTemperatureSetpointLow" 12.8 -execute $NODE_ID ThermostatSetMode "eco" -test_payload ".thermostatMode" '"eco"' - -execute $NODE_ID ThermostatTemperatureSetpoint 15.67 +execute $NODE_ID ThermostatSetMode "off" +test_payload ".thermostatMode" '"off"' test_payload ".thermostatTemperatureSetpoint" 15.67 test_payload ".thermostatTemperatureSetpointHigh" null test_payload ".thermostatTemperatureSetpointLow" null +execute $NODE_ID ThermostatSetMode "NOT_VALID" +test_payload ".thermostatMode" '"off"' + execute $NODE_ID TemperatureRelative 20 # test_payload ".thermostatTemperatureSetpoint" 35.67