diff --git a/front/src/assets/integrations/devices/netatmo/netatmo-NRV.jpg b/front/src/assets/integrations/devices/netatmo/netatmo-NRV.jpg new file mode 100644 index 0000000000..8632bd9590 Binary files /dev/null and b/front/src/assets/integrations/devices/netatmo/netatmo-NRV.jpg differ diff --git a/server/services/netatmo/lib/device/netatmo.buildFeaturesSpecifEnergy.js b/server/services/netatmo/lib/device/netatmo.buildFeaturesSpecifEnergy.js index 1b308ec5f8..cfd9727c06 100644 --- a/server/services/netatmo/lib/device/netatmo.buildFeaturesSpecifEnergy.js +++ b/server/services/netatmo/lib/device/netatmo.buildFeaturesSpecifEnergy.js @@ -51,6 +51,29 @@ function buildFeatureBoilerStatus(name, externalId) { }; } +/** + * @description Transforms Netatmo feature as Gladys feature. Heating power request. + * @param {string} name - Name device from Netatmo. + * @param {string} externalId - Gladys external ID. + * @returns {object} Gladys feature or undefined. + * @example + * buildFeatureHeatingPowerRequest(device_name, 'netatmo:device_id'); + */ +function buildFeatureHeatingPowerRequest(name, externalId) { + return { + name: `Heating power request - ${name}`, + external_id: `${externalId}:heating_power_request`, + selector: `${externalId}:heating_power_request`, + category: DEVICE_FEATURE_CATEGORIES.SWITCH, + type: DEVICE_FEATURE_TYPES.SWITCH.BINARY, + read_only: true, + keep_history: true, + has_feedback: false, + min: 0, + max: 1, + }; +} + /** * @description Transforms Netatmo feature as Gladys feature. Plug connected boiler. * @param {string} name - Name device from Netatmo. @@ -77,5 +100,6 @@ function buildFeaturePlugConnectedBoiler(name, externalId) { module.exports = { buildFeatureThermSetpointTemperature, buildFeatureBoilerStatus, + buildFeatureHeatingPowerRequest, buildFeaturePlugConnectedBoiler, }; diff --git a/server/services/netatmo/lib/device/netatmo.convertDevice.js b/server/services/netatmo/lib/device/netatmo.convertDevice.js index 6ea334ae3a..df11739cab 100644 --- a/server/services/netatmo/lib/device/netatmo.convertDevice.js +++ b/server/services/netatmo/lib/device/netatmo.convertDevice.js @@ -6,6 +6,7 @@ const { buildFeatureRfStrength, buildFeatureWifiStrength } = require('./netatmo. const { buildFeatureThermSetpointTemperature, buildFeatureBoilerStatus, + buildFeatureHeatingPowerRequest, buildFeaturePlugConnectedBoiler, } = require('./netatmo.buildFeaturesSpecifEnergy'); @@ -26,14 +27,14 @@ function convertDevice(netatmoDevice) { case SUPPORTED_MODULE_TYPE.THERMOSTAT: { /* features common */ features.push(buildFeatureBattery(name, externalId)); - /* features common Netatmo Energy */ + /* features common Netatmo */ features.push(buildFeatureTemperature(name, externalId, 'temperature')); features.push(buildFeatureTemperature(`room ${room.name}`, externalId, 'therm_measured_temperature')); - features.push(buildFeatureThermSetpointTemperature(name, externalId)); - features.push(buildFeatureOpenWindow(name, externalId)); /* features common modules RF */ features.push(buildFeatureRfStrength(name, externalId)); /* features specific Energy */ + features.push(buildFeatureThermSetpointTemperature(name, externalId)); + features.push(buildFeatureOpenWindow(name, externalId)); features.push(buildFeatureBoilerStatus(name, externalId)); /* params */ params = [ @@ -54,6 +55,24 @@ function convertDevice(netatmoDevice) { params = [{ name: PARAMS.MODULES_BRIDGE_ID, value: JSON.stringify(modulesBridged) }]; break; } + case SUPPORTED_MODULE_TYPE.NRV: { + /* features common */ + features.push(buildFeatureBattery(name, externalId)); + /* features common Netatmo */ + features.push(buildFeatureTemperature(`room ${room.name}`, externalId, 'therm_measured_temperature')); + /* features common modules RF */ + features.push(buildFeatureRfStrength(name, externalId)); + /* features specific Energy */ + features.push(buildFeatureThermSetpointTemperature(name, externalId)); + features.push(buildFeatureOpenWindow(name, externalId)); + features.push(buildFeatureHeatingPowerRequest(name, externalId)); + /* params */ + params = [ + { name: PARAMS.PLUG_ID, value: plug.id }, + { name: PARAMS.PLUG_NAME, value: plug.name }, + ]; + break; + } default: break; } diff --git a/server/services/netatmo/lib/device/netatmo.deviceMapping.js b/server/services/netatmo/lib/device/netatmo.deviceMapping.js index 3400c807b1..08a47b91ed 100644 --- a/server/services/netatmo/lib/device/netatmo.deviceMapping.js +++ b/server/services/netatmo/lib/device/netatmo.deviceMapping.js @@ -25,8 +25,19 @@ const readValues = { }, [DEVICE_FEATURE_CATEGORIES.BATTERY]: { /* battery_percent: 76 */ + /* battery_state: 'medium' */ [DEVICE_FEATURE_TYPES.BATTERY.INTEGER]: (valueFromDevice) => { - const valueToGladys = parseInt(valueFromDevice, 10); + const batteryLevels = { + max: 100, + full: 90, + high: 75, + medium: 50, + low: 25, + 'very low': 10, + }; + + const valueToGladys = + batteryLevels[valueFromDevice] !== undefined ? batteryLevels[valueFromDevice] : parseInt(valueFromDevice, 10); return valueToGladys; }, }, diff --git a/server/services/netatmo/lib/device/netatmo.updateNAPlug.js b/server/services/netatmo/lib/device/netatmo.updateNAPlug.js index ffa1a79dea..7b8e7f7362 100644 --- a/server/services/netatmo/lib/device/netatmo.updateNAPlug.js +++ b/server/services/netatmo/lib/device/netatmo.updateNAPlug.js @@ -15,7 +15,7 @@ async function updateNAPlug(deviceGladys, deviceNetatmo, externalId) { .filter((feature) => feature.external_id === `${externalId}:rf_strength`) .forEach((feature) => { this.gladys.event.emit(EVENTS.DEVICE.NEW_STATE, { - device_feature_external_id: `${externalId}:rf_strength`, + device_feature_external_id: feature.external_id, state: readValues[feature.category][feature.type](deviceNetatmo.rf_strength), }); }); @@ -24,7 +24,7 @@ async function updateNAPlug(deviceGladys, deviceNetatmo, externalId) { .filter((feature) => feature.external_id === `${externalId}:wifi_strength`) .forEach((feature) => { this.gladys.event.emit(EVENTS.DEVICE.NEW_STATE, { - device_feature_external_id: `${externalId}:wifi_strength`, + device_feature_external_id: feature.external_id, state: readValues[feature.category][feature.type](deviceNetatmo.wifi_strength), }); }); @@ -33,12 +33,12 @@ async function updateNAPlug(deviceGladys, deviceNetatmo, externalId) { .forEach((feature) => { if (typeof deviceNetatmo.plug_connected_boiler !== 'undefined') { this.gladys.event.emit(EVENTS.DEVICE.NEW_STATE, { - device_feature_external_id: `${externalId}:plug_connected_boiler`, + device_feature_external_id: feature.external_id, state: readValues[feature.category][feature.type](deviceNetatmo.plug_connected_boiler), }); } else { this.gladys.event.emit(EVENTS.DEVICE.NEW_STATE, { - device_feature_external_id: `${externalId}:plug_connected_boiler`, + device_feature_external_id: feature.external_id, state: readValues[feature.category][feature.type](false), }); } diff --git a/server/services/netatmo/lib/device/netatmo.updateNATherm1.js b/server/services/netatmo/lib/device/netatmo.updateNATherm1.js index c44d81f347..67903b201a 100644 --- a/server/services/netatmo/lib/device/netatmo.updateNATherm1.js +++ b/server/services/netatmo/lib/device/netatmo.updateNATherm1.js @@ -16,7 +16,7 @@ async function updateNATherm1(deviceGladys, deviceNetatmo, externalId) { .filter((feature) => feature.external_id === `${externalId}:battery_percent`) .forEach((feature) => { this.gladys.event.emit(EVENTS.DEVICE.NEW_STATE, { - device_feature_external_id: `${externalId}:battery_percent`, + device_feature_external_id: feature.external_id, state: readValues[feature.category][feature.type](deviceNetatmo.battery_percent), }); }); @@ -24,7 +24,7 @@ async function updateNATherm1(deviceGladys, deviceNetatmo, externalId) { .filter((feature) => feature.external_id === `${externalId}:temperature`) .forEach((feature) => { this.gladys.event.emit(EVENTS.DEVICE.NEW_STATE, { - device_feature_external_id: `${externalId}:temperature`, + device_feature_external_id: feature.external_id, state: readValues[feature.category][feature.type](measured.temperature), }); }); @@ -32,7 +32,7 @@ async function updateNATherm1(deviceGladys, deviceNetatmo, externalId) { .filter((feature) => feature.external_id === `${externalId}:therm_measured_temperature`) .forEach((feature) => { this.gladys.event.emit(EVENTS.DEVICE.NEW_STATE, { - device_feature_external_id: `${externalId}:therm_measured_temperature`, + device_feature_external_id: feature.external_id, state: readValues[feature.category][feature.type](room.therm_measured_temperature), }); }); @@ -40,7 +40,7 @@ async function updateNATherm1(deviceGladys, deviceNetatmo, externalId) { .filter((feature) => feature.external_id === `${externalId}:therm_setpoint_temperature`) .forEach((feature) => { this.gladys.event.emit(EVENTS.DEVICE.NEW_STATE, { - device_feature_external_id: `${externalId}:therm_setpoint_temperature`, + device_feature_external_id: feature.external_id, state: readValues[feature.category][feature.type](room.therm_setpoint_temperature), }); }); @@ -48,7 +48,7 @@ async function updateNATherm1(deviceGladys, deviceNetatmo, externalId) { .filter((feature) => feature.external_id === `${externalId}:open_window`) .forEach((feature) => { this.gladys.event.emit(EVENTS.DEVICE.NEW_STATE, { - device_feature_external_id: `${externalId}:open_window`, + device_feature_external_id: feature.external_id, state: readValues[feature.category][feature.type](room.open_window), }); }); @@ -56,7 +56,7 @@ async function updateNATherm1(deviceGladys, deviceNetatmo, externalId) { .filter((feature) => feature.external_id === `${externalId}:rf_strength`) .forEach((feature) => { this.gladys.event.emit(EVENTS.DEVICE.NEW_STATE, { - device_feature_external_id: `${externalId}:rf_strength`, + device_feature_external_id: feature.external_id, state: readValues[feature.category][feature.type](deviceNetatmo.rf_strength), }); }); @@ -64,7 +64,7 @@ async function updateNATherm1(deviceGladys, deviceNetatmo, externalId) { .filter((feature) => feature.external_id === `${externalId}:boiler_status`) .forEach((feature) => { this.gladys.event.emit(EVENTS.DEVICE.NEW_STATE, { - device_feature_external_id: `${externalId}:boiler_status`, + device_feature_external_id: feature.external_id, state: readValues[feature.category][feature.type](deviceNetatmo.boiler_status), }); }); diff --git a/server/services/netatmo/lib/device/netatmo.updateNRV.js b/server/services/netatmo/lib/device/netatmo.updateNRV.js new file mode 100644 index 0000000000..ef09d97e49 --- /dev/null +++ b/server/services/netatmo/lib/device/netatmo.updateNRV.js @@ -0,0 +1,71 @@ +const { EVENTS } = require('../../../../utils/constants'); +const logger = require('../../../../utils/logger'); +const { readValues } = require('./netatmo.deviceMapping'); + +/** + * @description Save values of valves NRV. + * @param {object} deviceGladys - Device object in Gladys. + * @param {object} deviceNetatmo - Device object coming from the Netatmo API. + * @param {string} externalId - Device identifier in gladys. + * @example updateNRV(deviceGladys, deviceNetatmo, externalId); + */ +async function updateNRV(deviceGladys, deviceNetatmo, externalId) { + const { room } = deviceNetatmo; + try { + deviceGladys.features + .filter((feature) => feature.external_id === `${externalId}:battery_percent`) + .forEach((feature) => { + this.gladys.event.emit(EVENTS.DEVICE.NEW_STATE, { + device_feature_external_id: feature.external_id, + state: readValues[feature.category][feature.type](deviceNetatmo.battery_state), + }); + }); + deviceGladys.features + .filter((feature) => feature.external_id === `${externalId}:therm_measured_temperature`) + .forEach((feature) => { + this.gladys.event.emit(EVENTS.DEVICE.NEW_STATE, { + device_feature_external_id: feature.external_id, + state: readValues[feature.category][feature.type](room.therm_measured_temperature), + }); + }); + deviceGladys.features + .filter((feature) => feature.external_id === `${externalId}:therm_setpoint_temperature`) + .forEach((feature) => { + this.gladys.event.emit(EVENTS.DEVICE.NEW_STATE, { + device_feature_external_id: feature.external_id, + state: readValues[feature.category][feature.type](room.therm_setpoint_temperature), + }); + }); + deviceGladys.features + .filter((feature) => feature.external_id === `${externalId}:open_window`) + .forEach((feature) => { + this.gladys.event.emit(EVENTS.DEVICE.NEW_STATE, { + device_feature_external_id: feature.external_id, + state: readValues[feature.category][feature.type](room.open_window), + }); + }); + deviceGladys.features + .filter((feature) => feature.external_id === `${externalId}:rf_strength`) + .forEach((feature) => { + this.gladys.event.emit(EVENTS.DEVICE.NEW_STATE, { + device_feature_external_id: feature.external_id, + state: readValues[feature.category][feature.type](deviceNetatmo.rf_strength), + }); + }); + deviceGladys.features + .filter((feature) => feature.external_id === `${externalId}:heating_power_request`) + .forEach((feature) => { + const value = room.heating_power_request > 0; + this.gladys.event.emit(EVENTS.DEVICE.NEW_STATE, { + device_feature_external_id: feature.external_id, + state: readValues[feature.category][feature.type](value), + }); + }); + } catch (e) { + logger.error('deviceGladys NRV: ', deviceGladys.name, 'error: ', e); + } +} + +module.exports = { + updateNRV, +}; diff --git a/server/services/netatmo/lib/index.js b/server/services/netatmo/lib/index.js index 3678e2d23f..284fb20973 100644 --- a/server/services/netatmo/lib/index.js +++ b/server/services/netatmo/lib/index.js @@ -20,6 +20,7 @@ const { setValue } = require('./netatmo.setValue'); const { updateValues } = require('./netatmo.updateValues'); const { updateNAPlug } = require('./device/netatmo.updateNAPlug'); const { updateNATherm1 } = require('./device/netatmo.updateNATherm1'); +const { updateNRV } = require('./device/netatmo.updateNRV'); const { STATUS, SCOPES } = require('./utils/netatmo.constants'); const buildScopesConfig = require('./utils/netatmo.buildScopesConfig'); @@ -66,5 +67,6 @@ NetatmoHandler.prototype.setValue = setValue; NetatmoHandler.prototype.updateValues = updateValues; NetatmoHandler.prototype.updateNAPlug = updateNAPlug; NetatmoHandler.prototype.updateNATherm1 = updateNATherm1; +NetatmoHandler.prototype.updateNRV = updateNRV; module.exports = NetatmoHandler; diff --git a/server/services/netatmo/lib/netatmo.loadDeviceDetails.js b/server/services/netatmo/lib/netatmo.loadDeviceDetails.js index ac36411dd2..e33788f084 100644 --- a/server/services/netatmo/lib/netatmo.loadDeviceDetails.js +++ b/server/services/netatmo/lib/netatmo.loadDeviceDetails.js @@ -74,6 +74,10 @@ async function loadDeviceDetails(homeData) { moduleSupported = true; categoryAPI = SUPPORTED_CATEGORY_TYPE.ENERGY; break; + case SUPPORTED_MODULE_TYPE.NRV: + moduleSupported = true; + categoryAPI = SUPPORTED_CATEGORY_TYPE.ENERGY; + break; default: moduleSupported = false; break; diff --git a/server/services/netatmo/lib/netatmo.updateValues.js b/server/services/netatmo/lib/netatmo.updateValues.js index 9bc7be7e3c..7722e798cd 100644 --- a/server/services/netatmo/lib/netatmo.updateValues.js +++ b/server/services/netatmo/lib/netatmo.updateValues.js @@ -25,6 +25,10 @@ async function updateValues(deviceGladys, deviceNetatmo, externalId) { await this.updateNATherm1(deviceGladys, deviceNetatmo, externalId); break; } + case SUPPORTED_MODULE_TYPE.NRV: { + await this.updateNRV(deviceGladys, deviceNetatmo, externalId); + break; + } default: break; } diff --git a/server/services/netatmo/lib/utils/netatmo.constants.js b/server/services/netatmo/lib/utils/netatmo.constants.js index bad1368143..fa6140370c 100644 --- a/server/services/netatmo/lib/utils/netatmo.constants.js +++ b/server/services/netatmo/lib/utils/netatmo.constants.js @@ -72,6 +72,7 @@ const SUPPORTED_CATEGORY_TYPE = { const SUPPORTED_MODULE_TYPE = { THERMOSTAT: 'NATherm1', PLUG: 'NAPlug', + NRV: 'NRV', }; const PARAMS = { diff --git a/server/test/services/netatmo/lib/device/netatmo.convertDevice.test.js b/server/test/services/netatmo/lib/device/netatmo.convertDevice.test.js index 1f5005b658..20252883e3 100644 --- a/server/test/services/netatmo/lib/device/netatmo.convertDevice.test.js +++ b/server/test/services/netatmo/lib/device/netatmo.convertDevice.test.js @@ -3,7 +3,7 @@ const sinon = require('sinon'); const { convertDevice } = require('../../../../../services/netatmo/lib/device/netatmo.convertDevice'); const { SUPPORTED_MODULE_TYPE } = require('../../../../../services/netatmo/lib/utils/netatmo.constants'); -const devicesMock = require('../../netatmo.loadDevices.mock.test.json'); +const devicesNetatmoMock = require('../../netatmo.loadDevices.mock.test.json'); const devicesGladysMock = require('../../netatmo.convertDevices.mock.test.json'); const NetatmoHandler = require('../../../../../services/netatmo/lib/index'); @@ -22,23 +22,23 @@ describe('Netatmo Convert Device', () => { it('should correctly convert a Netatmo Plug device', () => { const deviceGladysMock = devicesGladysMock.filter((device) => device.model === 'NAPlug')[0]; - const deviceMock = devicesMock.filter((device) => device.type === 'NAPlug')[0]; + const deviceNetatmoMock = devicesNetatmoMock.filter((device) => device.type === 'NAPlug')[0]; - const gladysDevice = convertDevice.bind(netatmoHandler)(deviceMock); + const gladysDevice = convertDevice.bind(netatmoHandler)(deviceNetatmoMock); expect(gladysDevice).deep.equal(deviceGladysMock); expect(gladysDevice.features).deep.equal(deviceGladysMock.features); expect(gladysDevice.params).deep.equal(deviceGladysMock.params); expect(gladysDevice).to.have.property('name', deviceGladysMock.name); - expect(gladysDevice).to.have.property('external_id', `netatmo:${deviceMock.id}`); + expect(gladysDevice).to.have.property('external_id', `netatmo:${deviceNetatmoMock.id}`); expect(gladysDevice).to.have.property('model', SUPPORTED_MODULE_TYPE.PLUG); const featureMock = gladysDevice.features.filter((feature) => feature.name.includes('connected boiler'))[0]; - expect(featureMock).to.have.property('external_id', `netatmo:${deviceMock.id}:plug_connected_boiler`); + expect(featureMock).to.have.property('external_id', `netatmo:${deviceNetatmoMock.id}:plug_connected_boiler`); const paramMock = gladysDevice.params.filter((param) => param.name === 'modules_bridge_id')[0]; - expect(paramMock).to.have.property('value', JSON.stringify(deviceMock.modules_bridged)); + expect(paramMock).to.have.property('value', JSON.stringify(deviceNetatmoMock.modules_bridged)); expect(gladysDevice.features).to.be.an('array'); expect(gladysDevice.params).to.be.an('array'); @@ -46,23 +46,47 @@ describe('Netatmo Convert Device', () => { it('should correctly convert a Netatmo Thermostat device', () => { const deviceGladysMock = devicesGladysMock.filter((device) => device.model === 'NATherm1')[0]; - const deviceMock = devicesMock.filter((device) => device.type === 'NATherm1')[0]; + const deviceNetatmoMock = devicesNetatmoMock.filter((device) => device.type === 'NATherm1')[0]; - const gladysDevice = convertDevice.bind(netatmoHandler)(deviceMock); + const gladysDevice = convertDevice.bind(netatmoHandler)(deviceNetatmoMock); expect(gladysDevice).deep.equal(deviceGladysMock); expect(gladysDevice.features).deep.equal(deviceGladysMock.features); expect(gladysDevice.params).deep.equal(deviceGladysMock.params); expect(gladysDevice).to.have.property('name', deviceGladysMock.name); - expect(gladysDevice).to.have.property('external_id', `netatmo:${deviceMock.id}`); + expect(gladysDevice).to.have.property('external_id', `netatmo:${deviceNetatmoMock.id}`); expect(gladysDevice).to.have.property('model', SUPPORTED_MODULE_TYPE.THERMOSTAT); const featureMock = gladysDevice.features.filter((feature) => feature.type === 'target-temperature')[0]; - expect(featureMock).to.have.property('external_id', `netatmo:${deviceMock.id}:therm_setpoint_temperature`); + expect(featureMock).to.have.property('external_id', `netatmo:${deviceNetatmoMock.id}:therm_setpoint_temperature`); const paramMock = gladysDevice.params.filter((param) => param.name === 'plug_name')[0]; - expect(paramMock).to.have.property('value', deviceMock.plug.name); + expect(paramMock).to.have.property('value', deviceNetatmoMock.plug.name); + + expect(gladysDevice.features).to.be.an('array'); + expect(gladysDevice.params).to.be.an('array'); + }); + + it('should correctly convert a Netatmo Valve device', () => { + const deviceGladysMock = devicesGladysMock.filter((device) => device.model === 'NRV')[0]; + const deviceNetatmoMock = devicesNetatmoMock.filter((device) => device.type === 'NRV')[0]; + + const gladysDevice = convertDevice.bind(netatmoHandler)(deviceNetatmoMock); + + expect(gladysDevice).deep.equal(deviceGladysMock); + expect(gladysDevice.features).deep.equal(deviceGladysMock.features); + expect(gladysDevice.params).deep.equal(deviceGladysMock.params); + + expect(gladysDevice).to.have.property('name', deviceGladysMock.name); + expect(gladysDevice).to.have.property('external_id', `netatmo:${deviceNetatmoMock.id}`); + expect(gladysDevice).to.have.property('model', SUPPORTED_MODULE_TYPE.NRV); + + const featureMock = gladysDevice.features.filter((feature) => feature.type === 'target-temperature')[0]; + expect(featureMock).to.have.property('external_id', `netatmo:${deviceNetatmoMock.id}:therm_setpoint_temperature`); + + const paramMock = gladysDevice.params.filter((param) => param.name === 'plug_name')[0]; + expect(paramMock).to.have.property('value', deviceNetatmoMock.plug.name); expect(gladysDevice.features).to.be.an('array'); expect(gladysDevice.params).to.be.an('array'); @@ -70,20 +94,20 @@ describe('Netatmo Convert Device', () => { it('should correctly convert a Netatmo device not supported', () => { const deviceGladysMock = devicesGladysMock.filter((device) => device.not_handled)[0]; - const deviceMock = devicesMock.filter((device) => device.type === 'NOC')[0]; + const deviceNetatmoMock = devicesNetatmoMock.filter((device) => device.type === 'NOC')[0]; - const gladysDevice = convertDevice.bind(netatmoHandler)(deviceMock); + const gladysDevice = convertDevice.bind(netatmoHandler)(deviceNetatmoMock); expect(gladysDevice).deep.equal(deviceGladysMock); expect(gladysDevice.features).deep.equal([]); expect(gladysDevice.params).deep.equal(deviceGladysMock.params); expect(gladysDevice).to.have.property('name', deviceGladysMock.name); - expect(gladysDevice).to.have.property('external_id', `netatmo:${deviceMock.id}`); + expect(gladysDevice).to.have.property('external_id', `netatmo:${deviceNetatmoMock.id}`); expect(gladysDevice).to.have.property('model', 'NOC'); const paramMock = gladysDevice.params.filter((param) => param.name === 'room_name')[0]; - expect(paramMock).to.have.property('value', deviceMock.room.name); + expect(paramMock).to.have.property('value', deviceNetatmoMock.room.name); expect(gladysDevice.features).to.be.an('array'); expect(gladysDevice.params).to.be.an('array'); @@ -91,20 +115,21 @@ describe('Netatmo Convert Device', () => { it('should correctly convert a Netatmo device without room and without modules_bridged', () => { const deviceGladysMock = devicesGladysMock.filter((device) => device.model === 'NAPlug')[1]; - const deviceMock = devicesMock.filter((device) => device.type === 'NAPlug')[1]; + const deviceNetatmoMock = devicesNetatmoMock.filter((device) => device.type === 'NAPlug')[1]; + deviceNetatmoMock.modules_bridged = undefined; - const gladysDevice = convertDevice.bind(netatmoHandler)(deviceMock); + const gladysDevice = convertDevice.bind(netatmoHandler)(deviceNetatmoMock); expect(gladysDevice).deep.equal(deviceGladysMock); expect(gladysDevice.features).deep.equal(deviceGladysMock.features); expect(gladysDevice.params).deep.equal(deviceGladysMock.params); expect(gladysDevice).to.have.property('name', deviceGladysMock.name); - expect(gladysDevice).to.have.property('external_id', `netatmo:${deviceMock.id}`); + expect(gladysDevice).to.have.property('external_id', `netatmo:${deviceNetatmoMock.id}`); expect(gladysDevice).to.have.property('model', SUPPORTED_MODULE_TYPE.PLUG); const featureMock = gladysDevice.features.filter((feature) => feature.name.includes('connected boiler'))[0]; - expect(featureMock).to.have.property('external_id', `netatmo:${deviceMock.id}:plug_connected_boiler`); + expect(featureMock).to.have.property('external_id', `netatmo:${deviceNetatmoMock.id}:plug_connected_boiler`); const paramMock = gladysDevice.params.filter((param) => param.name === 'modules_bridge_id')[0]; expect(paramMock).deep.equal({ name: 'modules_bridge_id', value: '[]' }); diff --git a/server/test/services/netatmo/lib/device/netatmo.updateNRV.test.js b/server/test/services/netatmo/lib/device/netatmo.updateNRV.test.js new file mode 100644 index 0000000000..c7acd8becc --- /dev/null +++ b/server/test/services/netatmo/lib/device/netatmo.updateNRV.test.js @@ -0,0 +1,115 @@ +const { expect } = require('chai'); +const sinon = require('sinon'); + +const { fake } = sinon; + +const devicesGladys = require('../../netatmo.convertDevices.mock.test.json'); +const devicesNetatmo = require('../../netatmo.loadDevices.mock.test.json'); +const { EVENTS } = require('../../../../../utils/constants'); +const NetatmoHandler = require('../../../../../services/netatmo/lib/index'); +const logger = require('../../../../../utils/logger'); + +const gladys = { + event: { + emit: fake.resolves(null), + }, + variable: { + setValue: fake.resolves(null), + }, +}; +const serviceId = 'serviceId'; + +const netatmoHandler = new NetatmoHandler(gladys, serviceId); + +describe('Netatmo update NRV features', () => { + const deviceGladysNRV = devicesGladys[3]; + const deviceNetatmoNRV = JSON.parse(JSON.stringify(devicesNetatmo[3])); + const externalIdNRV = `netatmo:${devicesNetatmo[3].id}`; + beforeEach(() => { + sinon.reset(); + + netatmoHandler.status = 'not_initialized'; + }); + + afterEach(() => { + sinon.reset(); + }); + + it('should save all values according to all cases with heating power request', async () => { + await netatmoHandler.updateNRV(deviceGladysNRV, deviceNetatmoNRV, externalIdNRV); + + expect(netatmoHandler.gladys.event.emit.callCount).to.equal(6); + sinon.assert.calledWith(netatmoHandler.gladys.event.emit, 'device.new-state', { + device_feature_external_id: `${deviceGladysNRV.external_id}:battery_percent`, + state: 90, + }); + expect( + netatmoHandler.gladys.event.emit.getCall(0).calledWith(EVENTS.DEVICE.NEW_STATE, { + device_feature_external_id: 'netatmo:09:00:00:xx:xx:xx:battery_percent', + state: 90, + }), + ).to.equal(true); + expect( + netatmoHandler.gladys.event.emit.getCall(1).calledWith(EVENTS.DEVICE.NEW_STATE, { + device_feature_external_id: 'netatmo:09:00:00:xx:xx:xx:therm_measured_temperature', + state: 18.5, + }), + ).to.equal(true); + expect( + netatmoHandler.gladys.event.emit.getCall(2).calledWith(EVENTS.DEVICE.NEW_STATE, { + device_feature_external_id: 'netatmo:09:00:00:xx:xx:xx:therm_setpoint_temperature', + state: 19, + }), + ).to.equal(true); + expect( + netatmoHandler.gladys.event.emit.getCall(3).calledWith(EVENTS.DEVICE.NEW_STATE, { + device_feature_external_id: 'netatmo:09:00:00:xx:xx:xx:open_window', + state: 0, + }), + ).to.equal(true); + expect( + netatmoHandler.gladys.event.emit.getCall(4).calledWith(EVENTS.DEVICE.NEW_STATE, { + device_feature_external_id: 'netatmo:09:00:00:xx:xx:xx:rf_strength', + state: 80, + }), + ).to.equal(true); + expect( + netatmoHandler.gladys.event.emit.getCall(5).calledWith(EVENTS.DEVICE.NEW_STATE, { + device_feature_external_id: 'netatmo:09:00:00:xx:xx:xx:heating_power_request', + state: 1, + }), + ).to.equal(true); + }); + + it('should save all values according to all cases without heating power request', async () => { + deviceNetatmoNRV.room.heating_power_request = 0; + + await netatmoHandler.updateNRV(deviceGladysNRV, deviceNetatmoNRV, externalIdNRV); + + expect(netatmoHandler.gladys.event.emit.callCount).to.equal(6); + sinon.assert.calledWith(netatmoHandler.gladys.event.emit, 'device.new-state', { + device_feature_external_id: `${deviceGladysNRV.external_id}:heating_power_request`, + state: 0, + }); + }); + + it('should handle errors correctly', async () => { + deviceNetatmoNRV.battery_percent = undefined; + const error = new Error('Test error'); + netatmoHandler.gladys = { + event: { + emit: sinon.stub().throws(error), + }, + }; + sinon.stub(logger, 'error'); + + try { + await netatmoHandler.updateNRV(deviceGladysNRV, deviceNetatmoNRV, externalIdNRV); + } catch (e) { + expect(e).to.equal(error); + sinon.assert.calledOnce(logger.error); + } + + logger.error.restore(); + }); +}); diff --git a/server/test/services/netatmo/lib/netatmo.loadDeviceDetails.test.js b/server/test/services/netatmo/lib/netatmo.loadDeviceDetails.test.js index e104b16a74..4b3c95df5b 100644 --- a/server/test/services/netatmo/lib/netatmo.loadDeviceDetails.test.js +++ b/server/test/services/netatmo/lib/netatmo.loadDeviceDetails.test.js @@ -46,7 +46,7 @@ describe('Netatmo Load Device Details', () => { .reply(200, { body: bodyHomeStatusMock, status: 'ok' }); const devices = await netatmoHandler.loadDeviceDetails(homesMock); - expect(devices).to.have.lengthOf(4); + expect(devices).to.have.lengthOf(5); expect(devices.filter((device) => device.type === 'NATherm1')).to.have.lengthOf(1); expect(devices.filter((device) => device.type === 'NAPlug')).to.have.lengthOf(2); expect(devices.filter((device) => device.not_handled)).to.have.lengthOf(1); @@ -78,7 +78,7 @@ describe('Netatmo Load Device Details', () => { const devices = await netatmoHandler.loadDeviceDetails(homesMockFake); - expect(devices).to.have.lengthOf(3); + expect(devices).to.have.lengthOf(4); expect(devices.filter((device) => device.type === 'NATherm1')).to.have.lengthOf(0); expect(devices.filter((device) => device.type === 'NAPlug')).to.have.lengthOf(2); expect(devices.filter((device) => device.not_handled)).to.have.lengthOf(1); diff --git a/server/test/services/netatmo/netatmo.convertDevices.mock.test.json b/server/test/services/netatmo/netatmo.convertDevices.mock.test.json index 20658eb19e..997a8193f8 100644 --- a/server/test/services/netatmo/netatmo.convertDevices.mock.test.json +++ b/server/test/services/netatmo/netatmo.convertDevices.mock.test.json @@ -110,6 +110,18 @@ "min": -10, "max": 50 }, + { + "name": "Link RF quality - Thermostat Test", + "external_id": "netatmo:04:00:00:xx:xx:xx:rf_strength", + "selector": "netatmo:04:00:00:xx:xx:xx:rf_strength", + "category": "signal", + "type": "integer", + "read_only": true, + "keep_history": true, + "has_feedback": false, + "min": 0, + "max": 100 + }, { "name": "Setpoint temperature - Thermostat Test", "external_id": "netatmo:04:00:00:xx:xx:xx:therm_setpoint_temperature", @@ -135,18 +147,6 @@ "min": 0, "max": 1 }, - { - "name": "Link RF quality - Thermostat Test", - "external_id": "netatmo:04:00:00:xx:xx:xx:rf_strength", - "selector": "netatmo:04:00:00:xx:xx:xx:rf_strength", - "category": "signal", - "type": "integer", - "read_only": true, - "keep_history": true, - "has_feedback": false, - "min": 0, - "max": 100 - }, { "name": "Boiler status - Thermostat Test", "external_id": "netatmo:04:00:00:xx:xx:xx:boiler_status", @@ -239,6 +239,113 @@ } ] }, + { + "name": "Valve Test", + "external_id": "netatmo:09:00:00:xx:xx:xx", + "selector": "netatmo:09:00:00:xx:xx:xx", + "model": "NRV", + "service_id": "serviceId", + "should_poll": false, + "features": [ + { + "name": "Battery - Valve Test", + "external_id": "netatmo:09:00:00:xx:xx:xx:battery_percent", + "selector": "netatmo:09:00:00:xx:xx:xx:battery_percent", + "category": "battery", + "type": "integer", + "unit": "percent", + "read_only": true, + "keep_history": true, + "has_feedback": false, + "min": 0, + "max": 100 + }, + { + "name": "Temperature - room Office", + "external_id": "netatmo:09:00:00:xx:xx:xx:therm_measured_temperature", + "selector": "netatmo:09:00:00:xx:xx:xx:therm_measured_temperature", + "category": "temperature-sensor", + "type": "decimal", + "unit": "celsius", + "read_only": true, + "keep_history": true, + "has_feedback": false, + "min": -10, + "max": 50 + }, + { + "name": "Link RF quality - Valve Test", + "external_id": "netatmo:09:00:00:xx:xx:xx:rf_strength", + "selector": "netatmo:09:00:00:xx:xx:xx:rf_strength", + "category": "signal", + "type": "integer", + "read_only": true, + "keep_history": true, + "has_feedback": false, + "min": 0, + "max": 100 + }, + { + "name": "Setpoint temperature - Valve Test", + "external_id": "netatmo:09:00:00:xx:xx:xx:therm_setpoint_temperature", + "selector": "netatmo:09:00:00:xx:xx:xx:therm_setpoint_temperature", + "category": "thermostat", + "type": "target-temperature", + "unit": "celsius", + "read_only": false, + "keep_history": true, + "has_feedback": false, + "min": 5, + "max": 30 + }, + { + "name": "Detecting open window - Valve Test", + "external_id": "netatmo:09:00:00:xx:xx:xx:open_window", + "selector": "netatmo:09:00:00:xx:xx:xx:open_window", + "category": "opening-sensor", + "type": "binary", + "read_only": true, + "keep_history": true, + "has_feedback": false, + "min": 0, + "max": 1 + }, + { + "name": "Heating power request - Valve Test", + "external_id": "netatmo:09:00:00:xx:xx:xx:heating_power_request", + "selector": "netatmo:09:00:00:xx:xx:xx:heating_power_request", + "category": "switch", + "type": "binary", + "read_only": true, + "keep_history": true, + "has_feedback": false, + "min": 0, + "max": 1 + } + ], + "params": [ + { + "name": "plug_id", + "value": "70:ee:50:yy:yy:yy" + }, + { + "name": "plug_name", + "value": "Relais Salon Test" + }, + { + "name": "home_id", + "value": "5e1xxxxxxxxxxxxxxxxx" + }, + { + "name": "room_id", + "value": "8765432109" + }, + { + "name": "room_name", + "value": "Office" + } + ] + }, { "name": "Outdoor Parking Camera", "external_id": "netatmo:70:ee:00:xx:xx:xx", diff --git a/server/test/services/netatmo/netatmo.homesdata.mock.test.json b/server/test/services/netatmo/netatmo.homesdata.mock.test.json index df06d1cc10..1c5c3cf573 100644 --- a/server/test/services/netatmo/netatmo.homesdata.mock.test.json +++ b/server/test/services/netatmo/netatmo.homesdata.mock.test.json @@ -24,7 +24,13 @@ "id": "8765432109", "name": "Extérieur", "type": "outdoor", - "module_ids": ["70:ee:00:xx:xx:xx", "02:00:00:xx:xx:xx", "05:00:00:xx:xx:xx", "06:00:00:xx:xx:xx"] + "module_ids": [ + "70:ee:00:xx:xx:xx", + "02:00:00:xx:xx:xx", + "05:00:00:xx:xx:xx", + "06:00:00:xx:xx:xx", + "09:00:00:xx:xx:xx" + ] } ], "modules": [ @@ -56,6 +62,14 @@ "type": "NAPlug", "name": "Relais Salon Test", "setup_date": 1578496638 + }, + { + "id": "09:00:00:xx:xx:xx", + "type": "NRV", + "name": "Valve Test", + "setup_date": 1705912152, + "room_id": "8765432109", + "bridge": "70:ee:50:yy:yy:yy" } ], "temperature_control_mode": "heating", diff --git a/server/test/services/netatmo/netatmo.homestatus.mock.test.json b/server/test/services/netatmo/netatmo.homestatus.mock.test.json index fa32dba3b7..93ce432305 100644 --- a/server/test/services/netatmo/netatmo.homestatus.mock.test.json +++ b/server/test/services/netatmo/netatmo.homestatus.mock.test.json @@ -77,6 +77,16 @@ "firmware_revision": 236, "rf_strength": 106, "wifi_strength": 66 + }, + { + "id": "09:00:00:xx:xx:xx", + "type": "NRV", + "battery_state": "full", + "battery_level": 2956, + "firmware_revision": 100, + "rf_strength": 80, + "reachable": true, + "bridge": "70:ee:50:yy:yy:yy" } ], "persons": [ diff --git a/server/test/services/netatmo/netatmo.loadDevices.mock.test.json b/server/test/services/netatmo/netatmo.loadDevices.mock.test.json index 35d3a0cb8c..cdc1e3c847 100644 --- a/server/test/services/netatmo/netatmo.loadDevices.mock.test.json +++ b/server/test/services/netatmo/netatmo.loadDevices.mock.test.json @@ -147,8 +147,48 @@ "firmware": 124, "last_status_store": 1601000000, "home": "5e1xxxxxxxxxxxxxxxxx", + "modules_bridged": ["09:00:00:xx:xx:xx", "09:00:00:yy:yy:yy", "09:00:00:zz:zz:zz"], "plug": null }, + { + "id": "09:00:00:xx:xx:xx", + "type": "NRV", + "battery_state": "full", + "battery_level": 2956, + "firmware_revision": 100, + "rf_strength": 80, + "reachable": true, + "bridge": "70:ee:50:yy:yy:yy", + "name": "Valve Test", + "setup_date": 1705912152, + "room_id": "8765432109", + "home": "5e1xxxxxxxxxxxxxxxxx", + "room": { + "id": "8765432109", + "name": "Office", + "type": "home_office", + "module_ids": ["09:00:00:xx:xx:xx"], + "reachable": true, + "anticipating": false, + "heating_power_request": 100, + "open_window": false, + "therm_measured_temperature": 18.5, + "therm_setpoint_temperature": 19, + "therm_setpoint_start_time": 1706421605, + "therm_setpoint_mode": "schedule" + }, + "plug": { + "id": "70:ee:50:yy:yy:yy", + "type": "NAPlug", + "firmware_revision": 124, + "rf_strength": 65, + "wifi_strength": 55, + "name": "Relais Salon Test", + "setup_date": 1581000000, + "room_id": "2812958323", + "modules_bridged": ["09:00:00:xx:xx:xx", "09:00:00:yy:yy:yy", "09:00:00:zz:zz:zz"] + } + }, { "id": "70:ee:00:xx:xx:xx", "type": "NOC", diff --git a/server/test/services/netatmo/netatmo.loadDevicesDetails.mock.test.json b/server/test/services/netatmo/netatmo.loadDevicesDetails.mock.test.json index 84b0f72df5..3c3bd406f7 100644 --- a/server/test/services/netatmo/netatmo.loadDevicesDetails.mock.test.json +++ b/server/test/services/netatmo/netatmo.loadDevicesDetails.mock.test.json @@ -165,6 +165,45 @@ }, "plug": null }, + { + "id": "09:00:00:xx:xx:xx", + "type": "NRV", + "battery_state": "full", + "battery_level": 2956, + "firmware_revision": 100, + "rf_strength": 80, + "reachable": true, + "bridge": "70:ee:50:yy:yy:yy", + "name": "Valve Test", + "setup_date": 1705912152, + "room_id": "8765432109", + "home": "5e1xxxxxxxxxxxxxxxxx", + "room": { + "id": "8765432109", + "name": "Office", + "type": "home_office", + "module_ids": ["09:00:00:xx:xx:xx"], + "reachable": true, + "anticipating": false, + "heating_power_request": 100, + "open_window": false, + "therm_measured_temperature": 18.5, + "therm_setpoint_temperature": 19, + "therm_setpoint_start_time": 1706421605, + "therm_setpoint_mode": "schedule" + }, + "plug": { + "id": "70:ee:50:yy:yy:yy", + "type": "NAPlug", + "firmware_revision": 124, + "rf_strength": 65, + "wifi_strength": 55, + "name": "Relais Salon Test", + "setup_date": 1581000000, + "room_id": "2812958323", + "modules_bridged": ["09:00:00:xx:xx:xx", "09:00:00:yy:yy:yy", "09:00:00:zz:zz:zz"] + } + }, { "id": "70:ee:00:xx:xx:xx", "type": "NOC",