diff --git a/LICENSE b/LICENSE index e5c0149..b69f467 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,8 @@ MIT License Copyright (c) 2016 angeloxx +Copyright (c) 2017 Simone Tisa + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index d479a95..cfd426f 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,20 @@ # MyHomePlugin -Legrand (BTicino) MyHome plugin for homebridge: https://github.com/nfarina/homebridge +Legrand (BTicino) MyHome plugin for homebridge: https://github.com/nfarina/homebridge This is a fork of https://github.com/angeloxx/homebridge-myhome, with added accessories and support for Elgato Eve history feature for contact, motion sensor and powermeter. Legrand MyHome (http://www.homesystems-legrandgroup.com/BtHomeSystems/home.action) is an Home Automation solution that can manage: - lighting (standard on/off/dimmed lights) - thermoregulation - curtains, doors - security systems +- contact and motion sensors With this plugin, the support of a IP gateway installed in your plant and a configuration of all installed systems (MyHome does not support the autodiscovery of the system) you can control it. You need to disable the OpenWebNet password-based authentication from the IP of the device that runs homebridge (ie. Raspberry) or set the auhentication to HMAC; HMAC authentication is supported by all recent IP gateways or older one with updated firmware (eg. F454 with v2 firmware). -# Installation (TBD) -Install plugin with npm install -g homebridge-myhome +# Installation +Install plugin with `npm install -g simont77/homebridge-myhome` Add platform within config.json of you homebridge instance: { @@ -21,7 +22,7 @@ Add platform within config.json of you homebridge instance: "platform": "MyHome Gateway", "ipaddress": "192.168.1.1", "password": "12345", - "discovery": false, + "setclock": true, "devices": [ /*Static list of devices*/ ] @@ -36,18 +37,7 @@ Add platform within config.json of you homebridge instance: "accessories": [] } -Restart homebridge -Enjoy! - -# Installation - -Run as root: - - npm -g install homebridge-myhome-tng - -Create your config.json file starting from the template (sample-config.json) and then: - - homebridge -U . +Restart homebridge. Sample log is: @@ -88,52 +78,7 @@ Sample log is: ## Configuration -Only the platforms section of the config.json file should be edited, the pair code and the port in bridge section can be ignored in most of the cases. A little sample: - - { - "platforms": [{ - "platform": "LegrandMyHome", - "ipaddress": "192.168.157.207", - "port": 20000, - "ownpassword": "12345", - "discovery": false, - "devices": [{ - "accessory": "MHRelay", - "name": "Bathroom Light", - "address": "0/1/5" - },{ - "accessory": "MHRelay", - "name": "Night hallway Light", - "address": "0/1/1" - },{ - "accessory": "MHRelay", - "name": "Office", - "address": "0/1/4" - },{ - "accessory": "MHDimmer", - "name": "Master bedroom Central", - "address": "0/1/2" - },{ - "accessory": "MHThermostat", - "name": "Living Room Thermostat", - "address": "21" - },{ - "accessory": "MHThermostatExternal", - "name": "External Thermo Sensor", - "address": "1" - }] - }], - "bridge": { - "username": "CC:22:3D:E3:CE:31", - "name": "MyHome HomeBridge Adapter", - "pin": "342-52-220", - "port": 51827 - }, - "description": "My MyHome Home System", - "accessories": [ - ] - } - +Only the platforms section of the config.json file should be edited, the pair code and the port in bridge section can be ignored in most of the cases. The first part of the config file contains details about the MyHome Gateway used to interface the IP network with the plant: "platforms": [{ @@ -141,14 +86,14 @@ The first part of the config file contains details about the MyHome Gateway used "ipaddress": "192.168.157.207", "port": 20000, "ownpassword": "12345", - "discovery": false, + "setclock": true, "devices": [{ You need to change: -- ipaddress: put the IP address or name of the MyHome Gateway (eg. F454 or MH201, I'm not so updated about all gateways that BTicino-Legrand releases after 2015); the IP should be static but in the future I can implement a UPNP dicovery because all gateways supports that method +- ipaddress: put the IP address or name of the MyHome Gateway (eg. F454 or MH201) - port: should be 20000 and keep this value - ownpassword: the OpenWebNet password, default is 12345 but everyone will suggest to you to change it with another password (4 to 9 digits), but you will keep the default one, I know... -- discovery: boolean value, not supported but in the future allows the gateway to discover the plant and detect most of devices +- setclock: set to true if you want your homebridge server to set the time of your gateway every hour - devices: list of installed devices The devices section contains the list of devices that will be managed. All devices contains three standard properties: @@ -159,8 +104,7 @@ The devices section contains the list of devices that will be managed. All devic ## Supported devices -* MHRelay: Standard (Lighting) Relay (eg. F411), address is B/A/PL (eg. 0/1/10) - * this device supports the definition of a custom frame for on and off command, so you can specify frame\_on and/or frame\_off: +* MHRelay: Standard (Lighting) Relay (eg. F411), address is B/A/PL (eg. 0/1/10). This device supports the definition of a custom frame for on and off command, so you can specify frame\_on and/or frame\_off: { "accessory": "MHRelay", @@ -180,12 +124,18 @@ The devices section contains the list of devices that will be managed. All devic * MHBlind: Standard Automation Relay (eg. F411, I need to check the F401), address is B/A/PL (eg. 0/1/10) * this device defines another property called "time" that defines the configured "stop time" in seconds; using this property the driver can evaluate the current position of the blind * MHBlindAdvanced: Advanced version of standard Blind (eg. F401 that manages internally the current position), address is B/A/PL (eg. 0/1/10) -* MHContactSensor: Dry Contact sensor (eg. 3477 or some burgalarm sensors), address range is 1-201 -* MHPowerMeter: (WILL BE SUPPORTED) -* MHAlarm: (WILL BE SUPPORTED) +* MHContactSensor: Dry Contact sensor (eg. 3477 or some burgalarm sensors), address range is 1-201. Supported types are "motion" and "contact". Elgato Eve history feature is supported. +* MHPowerMeter: Only supported with F421 Load Control Central, use "refresh" to set the update interval. Elgato Eve history feature is supported. +* MHAlarm: tested on central 3486. Zones for Away, Night and At Home activation are currently hard coded in plugin code. Alarm activation/deactivation from Homekit is not implemented for security reasons, so only monitor of the current status is supported +* MHTimedRelay: to issue temporized command to relays. Default duration set in "duration" +* MHControlledLoad: to control status of old generation Load Control outlets +* MHAux: to deliver AUX events to Homekit. Supported type are "leak", "gas" and contact". +* MHIrrigation: modified MHTimedRelay using the new Homekit irrigation service + +See sample-config.json for the additional parameters of each accessory. ## Tested devices -- F454v1 and MH201 as IP Gateway +- F454v1, MH200N and MH201 as IP Gateway - F411/2 as MHRelay, MHOutlet and MHCurtain - F401 as MHBlindAdvanced - F416U1 as MHDimmer @@ -196,15 +146,8 @@ The devices section contains the list of devices that will be managed. All devic - Groups are not managed -# TODOS - -- Reconnection and infinite retry -- Semi-auto discovery and/or read a plant configuration from MyHomeSuite configuration file -- Re-order the code -- IP Gateway discovery -- Group and General Command support # Disclaimer I'm furnishing this software "as is". I do not provide any warranty of the item whatsoever, whether express, implied, or statutory, including, but not limited to, any warranty of merchantability or fitness for a particular purpose or any warranty that the contents of the item will be error-free. -The development of this module is not supported by Legrand, BTicino or Apple. These vendors and me are not responsible for direct, indirect, incidental or consequential damages resulting from any defect, error or failure to perform. \ No newline at end of file +The development of this module is not supported by Legrand, BTicino or Apple. These vendors and me are not responsible for direct, indirect, incidental or consequential damages resulting from any defect, error or failure to perform. diff --git a/index.js b/index.js index bb8a9db..1d62796 100644 --- a/index.js +++ b/index.js @@ -1,19 +1,51 @@ -var path = require("path"); -var mh = require(path.join(__dirname,'/lib/mhclient')); -var sprintf = require("sprintf-js").sprintf, inherits = require("util").inherits, Promise = require('promise'); +/*jshint esversion: 6,node: true,-W041: false */ +var path = require("path"); +var mh = require(path.join(__dirname, '/lib/mhclient')); +var sprintf = require("sprintf-js").sprintf, inherits = require("util").inherits; var events = require('events'), util = require('util'), fs = require('fs'); var Accessory, Characteristic, Service, UUIDGen; +var moment = require('moment'); +var correctingInterval = require('correcting-interval'); +const version = require('./package.json').version; +const Format = require('util').format; +var hexToBase64 = function (val) { + return new Buffer(('' + val).replace(/[^0-9A-F]/ig, ''), 'hex').toString('base64'); +}; +var base64ToHex = function (val) { + if (!val) + return val; + return new Buffer(val, 'base64').toString('hex'); +}; +var numToHex = function (val, len) { + var s = Number(val >>> 0).toString(16); + if (s.length % 2 != 0) { + s = '0' + s; + } + if (len) { + return ('0000000000000' + s).slice(-1 * len); + } + return s; +}; +var swap16 = function (val) { + return ((val & 0xFF) << 8) + | ((val >>> 8) & 0xFF); +}; +var swap32 = function (val) { + return ((val & 0xFF) << 24) + | ((val & 0xFF00) << 8) + | ((val >>> 8) & 0xFF00) + | ((val >>> 24) & 0xFF); +}; module.exports = function (homebridge) { Service = homebridge.hap.Service; Characteristic = homebridge.hap.Characteristic; Accessory = homebridge.platformAccessory; UUIDGen = homebridge.hap.uuid; + var FakeGatoHistoryService = require('fakegato-history')(homebridge); - - - /* Try to map Elgato's outlet custom vars */ - LegrandMyHome.CurrentPowerConsumption = function() { + /* Try to map Elgato custom vars */ + LegrandMyHome.CurrentPowerConsumption = function () { Characteristic.call(this, 'Consumption', 'E863F10D-079E-48FF-8F27-9C2605A29F52'); this.setProps({ format: Characteristic.Formats.UINT16, @@ -24,22 +56,177 @@ module.exports = function (homebridge) { perms: [Characteristic.Perms.READ, Characteristic.Perms.NOTIFY] }); this.value = this.getDefaultValue(); - }; - LegrandMyHome.CurrentPowerConsumption.UUID = 'E863F10D-079E-48FF-8F27-9C2605A29F52'; + }; + LegrandMyHome.CurrentPowerConsumption.UUID = 'E863F10D-079E-48FF-8F27-9C2605A29F52'; inherits(LegrandMyHome.CurrentPowerConsumption, Characteristic); - LegrandMyHome.PowerMeterService = function(displayName, subtype) { - Service.call(this, displayName, '00000001-0000-1777-8000-775D67EC4377', subtype); - this.addCharacteristic(LegrandMyHome.CurrentPowerConsumption); + + LegrandMyHome.TotalConsumption = function () { + Characteristic.call(this, 'Energy', 'E863F10C-079E-48FF-8F27-9C2605A29F52'); + this.setProps({ + format: Characteristic.Formats.FLOAT, + unit: "kWh", + maxValue: 100000000000, + minValue: 0, + minStep: 0.001, + perms: [Characteristic.Perms.READ, Characteristic.Perms.NOTIFY] + }); + this.value = this.getDefaultValue(); + }; + LegrandMyHome.TotalConsumption.UUID = 'E863F10C-079E-48FF-8F27-9C2605A29F52'; + inherits(LegrandMyHome.TotalConsumption, Characteristic); + + LegrandMyHome.ResetTotal = function () { + Characteristic.call(this, 'Reset', 'E863F112-079E-48FF-8F27-9C2605A29F52'); + this.setProps({ + format: Characteristic.Formats.UINT32, + perms: [Characteristic.Perms.READ, Characteristic.Perms.NOTIFY, Characteristic.Perms.WRITE] + }); + this.value = this.getDefaultValue(); + }; + LegrandMyHome.ResetTotal.UUID = 'E863F112-079E-48FF-8F27-9C2605A29F52'; + inherits(LegrandMyHome.ResetTotal, Characteristic); + + LegrandMyHome.Sensitivity = function () { + Characteristic.call(this, 'Sensitivity', 'E863F120-079E-48FF-8F27-9C2605A29F52'); + this.setProps({ + format: Characteristic.Formats.UINT16, + maxValue: 7, + minValue: 0, + minStep: 1, + perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE] + }); + this.value = this.getDefaultValue(); + }; + LegrandMyHome.Sensitivity.UUID = 'E863F120-079E-48FF-8F27-9C2605A29F52'; + inherits(LegrandMyHome.Sensitivity, Characteristic); + + LegrandMyHome.Duration = function () { + Characteristic.call(this, 'Duration', 'E863F12D-079E-48FF-8F27-9C2605A29F52'); + this.setProps({ + format: Characteristic.Formats.UINT16, + maxValue: 3600, + minValue: 0, + minStep: 1, + perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE] + }); + this.value = this.getDefaultValue(); + }; + LegrandMyHome.Duration.UUID = 'E863F12D-079E-48FF-8F27-9C2605A29F52'; + inherits(LegrandMyHome.Duration, Characteristic); + + LegrandMyHome.LastActivation = function () { + Characteristic.call(this, 'LastActivation', 'E863F11A-079E-48FF-8F27-9C2605A29F52'); + this.setProps({ + format: Characteristic.Formats.UINT32, + perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY] + }); + this.value = this.getDefaultValue(); + }; + LegrandMyHome.LastActivation.UUID = 'E863F11A-079E-48FF-8F27-9C2605A29F52'; + inherits(LegrandMyHome.LastActivation, Characteristic); + + LegrandMyHome.TimesOpened = function () { + Characteristic.call(this, 'TimesOpened', 'E863F129-079E-48FF-8F27-9C2605A29F52'); + this.setProps({ + format: Characteristic.Formats.UINT32, + perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY] + }); + this.value = this.getDefaultValue(); + }; + LegrandMyHome.TimesOpened.UUID = 'E863F129-079E-48FF-8F27-9C2605A29F52'; + inherits(LegrandMyHome.TimesOpened, Characteristic); + + LegrandMyHome.Char118 = function () { + Characteristic.call(this, 'Char118', 'E863F118-079E-48FF-8F27-9C2605A29F52'); + this.setProps({ + format: Characteristic.Formats.UINT32, + perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY] + }); + this.value = this.getDefaultValue(); + }; + LegrandMyHome.Char118.UUID = 'E863F118-079E-48FF-8F27-9C2605A29F52'; + inherits(LegrandMyHome.Char118, Characteristic); + + LegrandMyHome.Char119 = function () { + Characteristic.call(this, 'Char119', 'E863F119-079E-48FF-8F27-9C2605A29F52'); + this.setProps({ + format: Characteristic.Formats.UINT32, + perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY] + }); + this.value = this.getDefaultValue(); + }; + LegrandMyHome.Char119.UUID = 'E863F119-079E-48FF-8F27-9C2605A29F52'; + inherits(LegrandMyHome.Char119, Characteristic); + + LegrandMyHome.Char131 = function () { + Characteristic.call(this, 'Char131', 'E863F131-079E-48FF-8F27-9C2605A29F52'); + this.setProps({ + format: Characteristic.Formats.DATA, + perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY] + }); + this.value = this.getDefaultValue(); + }; + LegrandMyHome.Char131.UUID = 'E863F131-079E-48FF-8F27-9C2605A29F52'; + inherits(LegrandMyHome.Char131, Characteristic); + + LegrandMyHome.Char11D = function () { + Characteristic.call(this, 'Char11D', 'E863F11D-079E-48FF-8F27-9C2605A29F52'); + this.setProps({ + format: Characteristic.Formats.DATA, + perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY] + }); + this.value = this.getDefaultValue(); + }; + LegrandMyHome.Char11D.UUID = 'E863F11D-079E-48FF-8F27-9C2605A29F52'; + inherits(LegrandMyHome.Char11D, Characteristic); + + LegrandMyHome.SimpleBoolean = function () { + Characteristic.call(this, 'Esegui', '6C716596-A86D-4810-86A7-6F258DE448C3'); + this.setProps({ + format: Characteristic.Formats.BOOL, + perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY] + }); + this.value = this.getDefaultValue(); + }; + LegrandMyHome.TimesOpened.UUID = '6C716596-A86D-4810-86A7-6F258DE448C3'; + inherits(LegrandMyHome.SimpleBoolean, Characteristic); + + LegrandMyHome.PowerMeterService = function (displayName, subtype) { + Service.call(this, displayName, '00000001-0000-1777-8000-775D67EC4377', subtype); + this.addCharacteristic(LegrandMyHome.CurrentPowerConsumption); + this.addCharacteristic(LegrandMyHome.TotalConsumption); + this.addCharacteristic(LegrandMyHome.ResetTotal); }; inherits(LegrandMyHome.PowerMeterService, Service); + LegrandMyHome.FakeGatoHistoryService = FakeGatoHistoryService; + inherits(LegrandMyHome.FakeGatoHistoryService, Service); + + LegrandMyHome.ControlledLoadService = function (displayName, subtype) { + Service.call(this, displayName, 'D43133F2-9BDE-4731-9FF2-B427189DCB4A', subtype); + this.addCharacteristic(Characteristic.OutletInUse); + this.addCharacteristic(Characteristic.Active); + }; + inherits(LegrandMyHome.ControlledLoadService, Service); + + LegrandMyHome.ScenarioService = function (displayName, subtype) { + Service.call(this, displayName, '524C7C75-1D86-44BA-A923-554BDA240EE5', subtype); + this.addCharacteristic(Characteristic.Active); + this.addCharacteristic(LegrandMyHome.SimpleBoolean); + }; + inherits(LegrandMyHome.ScenarioService, Service); + + LegrandMyHome.RainSensorService = function (displayName, subtype) { + Service.call(this, displayName, '9018CDC8-DEF9-49D5-A969-63F8CCAAB1A6', subtype); + this.addCharacteristic(Characteristic.CurrentRelativeHumidity); + }; + inherits(LegrandMyHome.RainSensorService, Service); + process.setMaxListeners(0); homebridge.registerPlatform("homebridge-myhome", "LegrandMyHome", LegrandMyHome); }; - - class LegrandMyHome { constructor(log, config, api) { this.log = log; @@ -48,21 +235,27 @@ class LegrandMyHome { this.ready = false; this.devices = []; this.lightBuses = []; - this.controller = new mh.MyHomeClient(config.ipaddress, config.port, config.ownpassword, this); + this.controller = new mh.MyHomeClient(config.ipaddress, config.port, config.ownpassword, config.setclock, this); this.config.devices.forEach(function (accessory) { this.log.info("LegrandMyHome: adds accessory"); accessory.parent = this; - if (accessory.accessory == 'MHRelay') this.devices.push(new MHRelay(this.log,accessory)) - if (accessory.accessory == 'MHBlind') this.devices.push(new MHBlind(this.log,accessory)) - if (accessory.accessory == 'MHBlindAdvanced') this.devices.push(new MHBlindAdvanced(this.log,accessory)) - if (accessory.accessory == 'MHOutlet') this.devices.push(new MHRelay(this.log,accessory)) - if (accessory.accessory == 'MHRelayLight') this.devices.push(new MHRelay(this.log,accessory)) - if (accessory.accessory == 'MHDimmer') this.devices.push(new MHDimmer(this.log,accessory)) - if (accessory.accessory == 'MHThermostat') this.devices.push(new MHThermostat(this.log,accessory)) - if (accessory.accessory == 'MHExternalThermometer') this.devices.push(new MHThermometer(this.log,accessory)) - if (accessory.accessory == 'MHContactSensor') this.devices.push(new MHContactSensor(this.log,accessory)) - /* if (accessory.accessory == 'MHButton') this.devices.push(new MHButton(this.log,accessory)) */ - /* if (accessory.accessory == 'MHPowerMeter') this.devices.push(new MHPowerMeter(this.log,accessory)) */ + if (accessory.accessory == 'MHRelay') this.devices.push(new MHRelay(this.log, accessory)); + if (accessory.accessory == 'MHBlind') this.devices.push(new MHBlind(this.log, accessory)); + if (accessory.accessory == 'MHBlindAdvanced') this.devices.push(new MHBlindAdvanced(this.log, accessory)); + if (accessory.accessory == 'MHOutlet') this.devices.push(new MHRelay(this.log, accessory)); + if (accessory.accessory == 'MHTimedRelay') this.devices.push(new MHTimedRelay(this.log, accessory)); + if (accessory.accessory == 'MHRain') this.devices.push(new MHRain(this.log, accessory)); + if (accessory.accessory == 'MHDimmer') this.devices.push(new MHDimmer(this.log, accessory)); + if (accessory.accessory == 'MHThermostat') this.devices.push(new MHThermostat(this.log, accessory)); + if (accessory.accessory == 'MHExternalThermometer') this.devices.push(new MHThermometer(this.log, accessory)); + if (accessory.accessory == 'MHDryContact') this.devices.push(new MHDryContact(this.log, accessory)); + if (accessory.accessory == 'MHAux') this.devices.push(new MHAux(this.log, accessory)); + if (accessory.accessory == 'MHScenario') this.devices.push(new MHScenario(this.log, accessory)); + if (accessory.accessory == 'MHPowerMeter') this.devices.push(new MHPowerMeter(this.log, accessory)); + if (accessory.accessory == 'MHAlarm') this.devices.push(new MHAlarm(this.log, accessory)); + if (accessory.accessory == 'MHControlledLoad') this.devices.push(new MHControlledLoad(this.log, accessory)); + if (accessory.accessory == 'MHIrrigation') this.devices.push(new MHIrrigation(this.log, accessory)); + }.bind(this)); this.log.info("LegrandMyHome for MyHome Gateway at " + config.ipaddress + ":" + config.port); this.controller.start(); @@ -74,67 +267,497 @@ class LegrandMyHome { onConnect() { this.devices.forEach(function (accessory) { - if (accessory.thermostatService !== undefined) this.controller.getThermostatStatus(accessory.address); - if (accessory.contactSensorService !== undefined) this.controller.getContactState(accessory.address); - if (accessory.windowCoveringPlusService !== undefined) this.controller.getAdvancedBlindSate(accessory.address); + if (accessory.thermostatService !== undefined) + this.controller.getThermostatStatus(accessory.address); + if (accessory.contactSensorService !== undefined || accessory.dryContactService !== undefined) + this.controller.getContactState(accessory.address); + if (accessory.windowCoveringPlusService !== undefined) + this.controller.getAdvancedBlindState(accessory.address); + if (accessory.lightBulbService !== undefined && accessory.pul == true) + this.controller.getRelayState(accessory.address); + if (accessory.rainService !== undefined && accessory.pul == true) + this.controller.getRelayState(accessory.address); + if (accessory.IrrigationService !== undefined && accessory.pul == true) + this.controller.getRelayState(accessory.address); + if (accessory.alarmService !== undefined) + this.controller.getAlarmState(); + if (accessory.scenarioService !== undefined) + this.controller.getScenarioState(accessory.address); }.bind(this)); } - onRelay(_address,_onoff) { - this.devices.forEach(function(accessory) { - if (accessory.address == _address && accessory.lightBulbService !== undefined) { - accessory.power = _onoff; - accessory.bri = _onoff * 100; - accessory.lightBulbService.getCharacteristic(Characteristic.On).getValue(null); - } - }.bind(this)); + onRelay(_address, _onoff) { + + var address = _address.split("/"); + if (address.length != 3) return ""; + var a = parseInt(address[1]), pl = parseInt(address[2]); + + if (pl != 0) + this.devices.forEach(function (accessory) { + if (accessory.address == _address && accessory.lightBulbService !== undefined) { + accessory.power = _onoff; + accessory.bri = _onoff * 100; + accessory.lightBulbService.getCharacteristic(Characteristic.On).getValue(null); + } + if (accessory.address == _address && accessory.rainService !== undefined) { + accessory.power = _onoff; + accessory.rainService.getCharacteristic(Characteristic.CurrentRelativeHumidity).getValue(null); + } + if (accessory.address == _address && accessory.OutletService !== undefined) { + accessory.power = _onoff; + accessory.OutletService.getCharacteristic(Characteristic.On).getValue(null); + } + if (accessory.address == _address && accessory.IrrigationService !== undefined) { + accessory.power = _onoff; + accessory.IrrigationService.getCharacteristic(Characteristic.Active).getValue(null); + accessory.IrrigationService.getCharacteristic(Characteristic.InUse).getValue(null); + } + }.bind(this)); + else + if (a == 0) + this.devices.forEach(function (accessory) { + if (accessory.lightBulbService !== undefined && accessory.pul == false) { + accessory.power = _onoff; + accessory.bri = _onoff * 100; + accessory.lightBulbService.getCharacteristic(Characteristic.On).getValue(null); + } + if (accessory.address == _address && accessory.rainService !== undefined) { + accessory.power = _onoff; + accessory.rainService.getCharacteristic(Characteristic.CurrentRelativeHumidity).getValue(null); + } + if (accessory.address == _address && accessory.OutletService !== undefined) { + accessory.power = _onoff; + accessory.OutletService.getCharacteristic(Characteristic.On).getValue(null); + } + }.bind(this)); + else + this.devices.forEach(function (accessory) { + if (accessory.ambient == a && accessory.lightBulbService !== undefined && accessory.pul == false) { + accessory.power = _onoff; + accessory.bri = _onoff * 100; + accessory.lightBulbService.getCharacteristic(Characteristic.On).getValue(null); + } + if (accessory.address == _address && accessory.rainService !== undefined) { + accessory.power = _onoff; + accessory.rainService.getCharacteristic(Characteristic.CurrentRelativeHumidity).getValue(null); + } + if (accessory.address == _address && accessory.OutletService !== undefined) { + accessory.power = _onoff; + accessory.OutletService.getCharacteristic(Characteristic.On).getValue(null); + } + }.bind(this)); } - onContactSensor(_address,_state) { - this.devices.forEach(function(accessory) { + onContactSensor(_address, _state) { + this.devices.forEach(function (accessory) { if (accessory.address == _address && accessory.contactSensorService !== undefined) { accessory.state = _state; accessory.contactSensorService.getCharacteristic(Characteristic.ContactSensorState).getValue(null); } }.bind(this)); - } - - onDimmer(_address,_level) { - this.devices.forEach(function(accessory) { - if (accessory.address == _address && accessory.lightBulbService !== undefined) { - accessory.power = (_level > 0) ? 1 : 0; - accessory.bri = _level; - accessory.lightBulbService.getCharacteristic(Characteristic.On).getValue(null); + } + + onScenarioEnable(_address, _state) { + this.devices.forEach(function (accessory) { + if (accessory.scenarioService !== undefined && accessory.address == _address) { + accessory.state = _state; + accessory.scenarioService.getCharacteristic(Characteristic.Active).getValue(null); + } + }.bind(this)); + } + + onScenarioRun(_address, _state) { + this.devices.forEach(function (accessory) { + if (accessory.scenarioService !== undefined && accessory.address == _address) { + accessory.running = _state; + accessory.scenarioService.getCharacteristic(LegrandMyHome.SimpleBoolean).getValue(null); + } + }.bind(this)); + } + + onRelayDuration(_address, _value) { + this.devices.forEach(function (accessory) { + if (accessory.address == _address && accessory.IrrigationService !== undefined) { + if (accessory.power) { + accessory.RemDuration = _value; + if (accessory.timerHandle != undefined) + clearInterval(accessory.timerHandle); + accessory.timerHandle = setInterval(function () { + accessory.IrrigationService.setCharacteristic(Characteristic.RemainingDuration, accessory.RemDuration); + accessory.RemDuration--; + if (accessory.RemDuration == 0) + clearInterval(accessory.timerHandle); + }.bind(this), 1000); + } } }.bind(this)); } - onSimpleBlind(_address,_value) { - this.devices.forEach(function(accessory) { - if (accessory.address == _address && accessory.windowCoveringService !== undefined) { - switch (_value) { - case 0: - accessory.state = Characteristic.PositionState.STOPPED; - accessory.evaluatePosition(); + + onDryContact(_address, _state) { + this.devices.forEach(function (accessory) { + if (accessory.address == _address && accessory.dryContactService !== undefined) { + + switch (accessory.type) { + case 'Contact': + accessory.state = _state; + accessory.dryContactService.getCharacteristic(Characteristic.ContactSensorState).getValue(null); + break; + case 'Leak': + accessory.state = _state; accessory.dryContactService.getCharacteristic(Characteristic.LeakDetected).getValue(null); break; - case 1: - accessory.state = Characteristic.PositionState.INCREASING; - accessory.evaluatePosition(); + case 'Motion': + if (_state == true) { + accessory.state = true; + accessory.dryContactService.getCharacteristic(Characteristic.MotionDetected).getValue(null); + clearTimeout(accessory.durationhandle); + accessory.durationhandle = setTimeout(function () { + accessory.state = false; + accessory.dryContactService.getCharacteristic(Characteristic.MotionDetected).getValue(null); + }.bind(this), accessory.duration * 1000); + } + else + if (accessory.firstGet == true) { + accessory.state = _state; + accessory.dryContactService.getCharacteristic(Characteristic.MotionDetected).getValue(null); + } break; - case 2: - accessory.state = Characteristic.PositionState.DECREASING; - accessory.evaluatePosition(); + default: + accessory.state = _state; + accessory.dryContactService.getCharacteristic(Characteristic.ContactSensorState).getValue(null); break; } - accessory.windowCoveringService.getCharacteristic(Characteristic.PositionState).getValue(null); - accessory.windowCoveringService.getCharacteristic(Characteristic.CurrentPosition).getValue(null); - accessory.windowCoveringService.getCharacteristic(Characteristic.TargetPosition).getValue(null); } }.bind(this)); } - onAdvancedBlind(_address,_action,_position) { - this.devices.forEach(function(accessory) { + onAUX(_address, _state) { + this.devices.forEach(function (accessory) { + if (accessory.address == _address && accessory.AUXService !== undefined) { + accessory.state = _state; + switch (accessory.type) { + case 'Contact': accessory.AUXService.getCharacteristic(Characteristic.ContactSensorState).getValue(null); + break; + case 'Leak': accessory.AUXService.getCharacteristic(Characteristic.LeakDetected).getValue(null); + break; + case 'Motion': accessory.AUXService.getCharacteristic(Characteristic.MotionDetected).getValue(null); + break; + case 'Gas': accessory.AUXService.getCharacteristic(Characteristic.CarbonMonoxideDetected).getValue(null); + break; + default: accessory.AUXService.getCharacteristic(Characteristic.ContactSensorState).getValue(null); + break; + } + } + }.bind(this)); + } + + onDimmer(_address, _level) { + var address = _address.split("/"); + if (address.length != 3) return ""; + var a = parseInt(address[1]), pl = parseInt(address[2]); + + if (pl != 0) + this.devices.forEach(function (accessory) { + if (accessory.address == _address && accessory.lightBulbService !== undefined) { + accessory.power = (_level > 0) ? 1 : 0; + accessory.bri = _level; + accessory.lightBulbService.getCharacteristic(Characteristic.On).getValue(null); + accessory.lightBulbService.getCharacteristic(Characteristic.Brightness).getValue(null); + } + }.bind(this)); + else + if (a == 0) + this.devices.forEach(function (accessory) { + if (accessory.lightBulbService !== undefined && accessory.pul == false) { + accessory.power = (_level > 0) ? 1 : 0; + accessory.bri = _level; + accessory.lightBulbService.getCharacteristic(Characteristic.On).getValue(null); + accessory.lightBulbService.getCharacteristic(Characteristic.Brightness).getValue(null); + } + }.bind(this)); + else + this.devices.forEach(function (accessory) { + if (accessory.ambient == a && accessory.lightBulbService !== undefined && accessory.pul == false) { + accessory.power = (_level > 0) ? 1 : 0; + accessory.bri = _level; + accessory.lightBulbService.getCharacteristic(Characteristic.On).getValue(null); + accessory.lightBulbService.getCharacteristic(Characteristic.Brightness).getValue(null); + } + }.bind(this)); + } + + onPowerMeter(_value) { + this.devices.forEach(function (accessory) { + if (accessory.powerMeterService !== undefined) { + accessory.value = _value; + } + }.bind(this)); + } + + onAlarm(_state) { + this.devices.forEach(function (accessory) { + if (accessory.alarmService !== undefined) { + if (_state == 3) { + accessory.active = false; + accessory.triggered = false; + } + else + if (_state == 1) + accessory.active = true; + if (_state == 4) + accessory.triggered = true; + if ((accessory.active == true && _state != 1)) { + accessory.state = _state; + accessory.alarmService.getCharacteristic(Characteristic.SecuritySystemCurrentState).getValue(null); + } + if ((accessory.active == false && _state != 4)) { + accessory.state = _state; + accessory.alarmService.getCharacteristic(Characteristic.SecuritySystemCurrentState).getValue(null); + } + + if (_state != 4) { + accessory.target = _state; + accessory.alarmService.getCharacteristic(Characteristic.SecuritySystemTargetState).getValue(null); + } + } + }.bind(this)); + } + + onAlarmFault(_state) { + this.devices.forEach(function (accessory) { + if (accessory.alarmService !== undefined) { + accessory.fault = _state; + accessory.alarmService.getCharacteristic(Characteristic.StatusFault).getValue(null); + } + }.bind(this)); + + } + + onAlarmTampered(_state) { + this.devices.forEach(function (accessory) { + if (accessory.alarmService !== undefined) { + accessory.tampered = _state; + accessory.alarmService.getCharacteristic(Characteristic.StatusTampered).getValue(null); + } + }.bind(this)); + + } + + onZoneActive(_zone, _state) { + this.devices.forEach(function (accessory) { + if (accessory.alarmService !== undefined) { + accessory.zone[_zone] = _state; + if (accessory.zone[0] == true && + accessory.zone[1] == true && + accessory.zone[2] == true && + accessory.zone[3] == true && + accessory.zone[4] == false && + accessory.zone[5] == false && + accessory.zone[6] == false && + accessory.zone[7] == false && + accessory.active == true && + accessory.triggered == false) { + accessory.state = Characteristic.SecuritySystemCurrentState.AWAY_ARM; + accessory.target = Characteristic.SecuritySystemTargetState.AWAY_ARM; + accessory.alarmService.getCharacteristic(Characteristic.SecuritySystemCurrentState).getValue(null); + accessory.alarmService.getCharacteristic(Characteristic.SecuritySystemTargetState).getValue(null); + } + + else + if (accessory.zone[0] == true && + accessory.zone[1] == false && + accessory.zone[2] == true && + accessory.zone[3] == false && + accessory.zone[4] == false && + accessory.zone[5] == false && + accessory.zone[6] == false && + accessory.zone[7] == false && + accessory.active == true && + accessory.triggered == false) { + accessory.state = Characteristic.SecuritySystemCurrentState.NIGHT_ARM; + accessory.target = Characteristic.SecuritySystemTargetState.NIGHT_ARM; + accessory.alarmService.getCharacteristic(Characteristic.SecuritySystemCurrentState).getValue(null); + accessory.alarmService.getCharacteristic(Characteristic.SecuritySystemTargetState).getValue(null); + } + else + if (accessory.active == true && accessory.triggered == false) { + accessory.state = Characteristic.SecuritySystemCurrentState.STAY_ARM; + accessory.target = Characteristic.SecuritySystemTargetState.STAY_ARM; + accessory.alarmService.getCharacteristic(Characteristic.SecuritySystemCurrentState).getValue(null); + accessory.alarmService.getCharacteristic(Characteristic.SecuritySystemTargetState).getValue(null); + } + } + }.bind(this)); + } + + onAlarmLowBattery(_state) { + this.devices.forEach(function (accessory) { + if (accessory.alarmBatteryService !== undefined) { + accessory.lowbattery = _state; + if (_state == 0) + accessory.batterylevel = 100; + else + accessory.batterylevel = 0; + accessory.alarmBatteryService.getCharacteristic(Characteristic.StatusLowBattery).getValue(null); + accessory.alarmBatteryService.getCharacteristic(Characteristic.BatteryLevel).getValue(null); + } + }.bind(this)); + + } + + onAlarmNetwork(_state) { + this.devices.forEach(function (accessory) { + if (accessory.alarmBatteryService !== undefined) { + accessory.batterycharging = _state; + accessory.alarmBatteryService.getCharacteristic(Characteristic.ChargingState).getValue(null); + } + }.bind(this)); + + } + + + onControlledLoad(_address, _value) { + this.devices.forEach(function (accessory) { + if (accessory.controlledLoad !== undefined) { + if (accessory.address == _address) { + switch (_value) { + case 0: + accessory.enabled = false; + accessory.controlledLoad.getCharacteristic(Characteristic.OutletInUse).getValue(null); + break; + case 1: + accessory.enabled = true; + accessory.controlledLoad.getCharacteristic(Characteristic.OutletInUse).getValue(null); + break; + case 2: + accessory.forced = 1; + accessory.controlledLoad.getCharacteristic(Characteristic.Active).getValue(null); + break; + case 3: + accessory.forced = 0; + accessory.controlledLoad.getCharacteristic(Characteristic.Active).getValue(null); + break; + } + } + } + }.bind(this)); + } + + onSimpleBlind(_address, _value) { + var address = _address.split("/"); + if (address.length != 3) return ""; + var a = parseInt(address[1]), pl = parseInt(address[2]); + + if (pl != 0) + this.devices.forEach(function (accessory) { + if (accessory.address == _address && accessory.windowCoveringService !== undefined) { + switch (_value) { + case 0: + accessory.state = Characteristic.PositionState.STOPPED; + accessory.evaluatePosition(); + clearInterval(accessory.updateTimer); + break; + case 1: + if (accessory.invert == false) + accessory.state = Characteristic.PositionState.INCREASING; + else + accessory.state = Characteristic.PositionState.DECREASING; + accessory.updateTimer = setInterval(function () { + accessory.evaluatePosition(); + accessory.windowCoveringService.getCharacteristic(Characteristic.CurrentPosition).getValue(null); + }.bind(accessory), 500); + break; + case 2: + if (accessory.invert == false) + accessory.state = Characteristic.PositionState.DECREASING; + else + accessory.state = Characteristic.PositionState.INCREASING; + accessory.updateTimer = setInterval(function () { + accessory.evaluatePosition(); + accessory.windowCoveringService.getCharacteristic(Characteristic.CurrentPosition).getValue(null); + }.bind(accessory), 500); + break; + } + accessory.windowCoveringService.getCharacteristic(Characteristic.PositionState).getValue(null); + accessory.windowCoveringService.getCharacteristic(Characteristic.CurrentPosition).getValue(null); + accessory.windowCoveringService.getCharacteristic(Characteristic.TargetPosition).getValue(null); + } + }.bind(this)); + else + if (a == 0) + this.devices.forEach(function (accessory) { + if (accessory.windowCoveringService !== undefined && accessory.pul == false) { + switch (_value) { + case 0: + accessory.state = Characteristic.PositionState.STOPPED; + accessory.evaluatePosition(); + clearInterval(accessory.updateTimer); + break; + case 1: + if (accessory.invert == false) + accessory.state = Characteristic.PositionState.INCREASING; + else + accessory.state = Characteristic.PositionState.DECREASING; + accessory.updateTimer = setInterval(function () { + accessory.evaluatePosition(); + accessory.windowCoveringService.getCharacteristic(Characteristic.CurrentPosition).getValue(null); + }.bind(accessory), 500); + break; + case 2: + if (accessory.invert == false) + accessory.state = Characteristic.PositionState.DECREASING; + else + accessory.state = Characteristic.PositionState.INCREASING; + accessory.updateTimer = setInterval(function () { + accessory.evaluatePosition(); + accessory.windowCoveringService.getCharacteristic(Characteristic.CurrentPosition).getValue(null); + }.bind(accessory), 500); + break; + } + accessory.windowCoveringService.getCharacteristic(Characteristic.PositionState).getValue(null); + accessory.windowCoveringService.getCharacteristic(Characteristic.CurrentPosition).getValue(null); + accessory.windowCoveringService.getCharacteristic(Characteristic.TargetPosition).getValue(null); + } + }.bind(this)); + else + this.devices.forEach(function (accessory) { + if (accessory.ambient == a && accessory.windowCoveringService !== undefined && accessory.pul == false) { + switch (_value) { + case 0: + accessory.state = Characteristic.PositionState.STOPPED; + accessory.evaluatePosition(); + clearInterval(accessory.updateTimer); + break; + case 1: + if (accessory.invert == false) + accessory.state = Characteristic.PositionState.INCREASING; + else + accessory.state = Characteristic.PositionState.DECREASING; + accessory.updateTimer = setInterval(function () { + accessory.evaluatePosition(); + accessory.windowCoveringService.getCharacteristic(Characteristic.CurrentPosition).getValue(null); + }.bind(accessory), 500); + break; + case 2: + if (accessory.invert == false) + accessory.state = Characteristic.PositionState.DECREASING; + else + accessory.state = Characteristic.PositionState.INCREASING; + accessory.updateTimer = setInterval(function () { + accessory.evaluatePosition(); + accessory.windowCoveringService.getCharacteristic(Characteristic.CurrentPosition).getValue(null); + }.bind(accessory), 500); + break; + } + accessory.windowCoveringService.getCharacteristic(Characteristic.PositionState).getValue(null); + accessory.windowCoveringService.getCharacteristic(Characteristic.CurrentPosition).getValue(null); + accessory.windowCoveringService.getCharacteristic(Characteristic.TargetPosition).getValue(null); + } + }.bind(this)); + } + + onAdvancedBlind(_address, _action, _position) { + this.devices.forEach(function (accessory) { if (accessory.address == _address && accessory.windowCoveringPlusService !== undefined) { if (_action == "STOP") { accessory.currentPosition = accessory.targetPosition = _position; @@ -159,8 +782,8 @@ class LegrandMyHome { }.bind(this)); } - onThermostat(_address,_measure,_level) { - this.devices.forEach(function(accessory) { + onThermostat(_address, _measure, _level) { + this.devices.forEach(function (accessory) { if (accessory.address == _address && accessory.thermostatService !== undefined) { if (_measure == "AMBIENT") { accessory.ambient = _level; @@ -192,24 +815,24 @@ class LegrandMyHome { accessory.thermostatService.getCharacteristic(Characteristic.TargetHeatingCoolingState).getValue(null); } } - }.bind(this)); + }.bind(this)); } - onThermometer(_address,_measure,_level) { - this.devices.forEach(function(accessory) { + onThermometer(_address, _measure, _level) { + this.devices.forEach(function (accessory) { if (accessory.address == _address && accessory.thermometerService !== undefined) { if (_measure == "AMBIENT") { accessory.ambient = _level; accessory.thermometerService.getCharacteristic(Characteristic.CurrentTemperature).getValue(null); } } - }.bind(this)); - } + }.bind(this)); + } accessories(callback) { this.log.debug("LegrandMyHome (accessories readed)"); callback(this.devices); - } + } } class MHRelay { @@ -219,9 +842,9 @@ class MHRelay { this.name = config.name; this.address = config.address; this.groups = config.groups || []; /* TODO */ - this.pul = false; /* TODO */ + this.pul = config.pul || false; this.displayName = config.name; - this.UUID = UUIDGen.generate(sprintf("relay-%s",config.address)); + this.UUID = UUIDGen.generate(sprintf("relay-%s", config.address)); this.log = log; this.power = false; this.bri = 100; @@ -229,17 +852,23 @@ class MHRelay { this.hue = 0; this.log.info(sprintf("LegrandMyHome::MHRelay create object: %s", this.address)); this.mh.addLightBusDevice(this.address); + + var address = this.address.split("/"); + this.bus = parseInt(address[0]); + this.ambient = parseInt(address[1]); + this.pl = parseInt(address[2]); } getServices() { var service = new Service.AccessoryInformation(); service.setCharacteristic(Characteristic.Name, this.name) - .setCharacteristic(Characteristic.Manufacturer, "Legrand MyHome") + .setCharacteristic(Characteristic.Manufacturer, "Simone Tisa") .setCharacteristic(Characteristic.Model, "Relay") + .setCharacteristic(Characteristic.FirmwareRevision, version) .setCharacteristic(Characteristic.SerialNumber, "Address " + this.address); switch (this.config.accessory) { - case 'MHRelayOutlet': + case 'MHOutlet': this.lightBulbService = new Service.Outlet(this.name); break; default: @@ -247,9 +876,10 @@ class MHRelay { break; } + this.lightBulbService.getCharacteristic(Characteristic.On) .on('set', (level, callback) => { - this.log.debug(sprintf("setPower %s = %s",this.address, level)); + this.log.debug(sprintf("setPower %s = %s", this.address, level)); this.power = (level > 0); if (this.power && this.bri == 0) { this.bri = 100; @@ -261,18 +891,135 @@ class MHRelay { } else if (!this.power && this.config.frame_off != null) { this.mh.send(this.config.frame_off); } else { - this.mh.relayCommand(this.address,this.power) + this.mh.relayCommand(this.address, this.power); } callback(null); }) .on('get', (callback) => { - this.log.debug(sprintf("getPower %s = %s",this.address, this.power)); + this.log.debug(sprintf("getPower %s = %s", this.address, this.power)); callback(null, this.power); }); + return [service, this.lightBulbService]; } } +class MHTimedRelay { + constructor(log, config) { + this.config = config || {}; + this.mh = config.parent.controller; + this.name = config.name; + this.address = config.address; + this.groups = config.groups || []; /* TODO */ + this.pul = config.pul || false; + this.displayName = config.name; + this.UUID = UUIDGen.generate(sprintf("timedrelay-%s", config.address)); + this.log = log; + this.power = false; + this.timer = config.timer || 1; + this.log.info(sprintf("LegrandMyHome::MHTimedRelay create object: %s", this.address)); + this.mh.addLightBusDevice(this.address); + + var address = this.address.split("/"); + this.bus = parseInt(address[0]); + this.ambient = parseInt(address[1]); + this.pl = parseInt(address[2]); + } + + getServices() { + var service = new Service.AccessoryInformation(); + service.setCharacteristic(Characteristic.Name, this.name) + .setCharacteristic(Characteristic.Manufacturer, "Simone Tisa") + .setCharacteristic(Characteristic.Model, "TimedRelay") + .setCharacteristic(Characteristic.FirmwareRevision, version) + .setCharacteristic(Characteristic.SerialNumber, "Address " + this.address); + + + this.OutletService = new Service.Outlet(this.name); + this.OutletService.addCharacteristic(Characteristic.LockManagementAutoSecurityTimeout); + this.OutletService.getCharacteristic(Characteristic.LockManagementAutoSecurityTimeout) + .setProps({ + format: Characteristic.Formats.UINT32, + unit: Characteristic.Units.SECONDS, + maxValue: 3540, + minValue: 0, + minStep: 60, + perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY] + }); + this.OutletService.getCharacteristic(Characteristic.On) + .on('set', (_value, callback) => { + //this.log.debug(sprintf("setPower %s = %s",this.address, level)); + this.power = _value; + + /* Custom frame support */ + if (this.power && this.timer != 0) { + this.mh.relayTimedOn(this.address, this.timer / 3600, this.timer / 60, this.timer % 60); + } + else { + this.mh.relayCommand(this.address, this.power); + } + callback(null); + }) + .on('get', (callback) => { + this.log.debug(sprintf("getPower %s = %s", this.address, this.power)); + callback(null, this.power); + }); + this.OutletService.getCharacteristic(Characteristic.LockManagementAutoSecurityTimeout) + .on('set', (time, callback) => { + this.timer = time; + callback(null); + }) + .on('get', (callback) => { + this.log.debug(sprintf("getPower %s = %s", this.address, this.power)); + callback(null, this.timer); + }); + + + return [service, this.OutletService]; + } +} + +class MHRain { + constructor(log, config) { + this.config = config || {}; + this.mh = config.parent.controller; + this.name = config.name; + this.address = config.address; + this.groups = config.groups || []; /* TODO */ + this.pul = config.pul || false; + this.displayName = config.name; + this.UUID = UUIDGen.generate(sprintf("relay-%s", config.address)); + this.log = log; + this.power = 0; + this.log.info(sprintf("LegrandMyHome::MHRain create object: %s", this.address)); + this.mh.addLightBusDevice(this.address); + + var address = this.address.split("/"); + this.bus = parseInt(address[0]); + this.ambient = parseInt(address[1]); + this.pl = parseInt(address[2]); + } + + getServices() { + var service = new Service.AccessoryInformation(); + service.setCharacteristic(Characteristic.Name, this.name) + .setCharacteristic(Characteristic.Manufacturer, "Simone Tisa") + .setCharacteristic(Characteristic.Model, "Relay") + .setCharacteristic(Characteristic.FirmwareRevision, version) + .setCharacteristic(Characteristic.SerialNumber, "Address " + this.address); + + + this.rainService = new LegrandMyHome.RainSensorService(this.name); + this.rainService.getCharacteristic(Characteristic.CurrentRelativeHumidity) + .on('get', (callback) => { + //this.log.debug(sprintf("getPower %s = %s",this.address, this.power)); + callback(null, 100 - this.power * 100); + }); + + return [service, this.rainService]; + } +} + class MHBlind { constructor(log, config) { this.config = config || {}; @@ -280,94 +1027,132 @@ class MHBlind { this.name = config.name; this.address = config.address; this.groups = config.groups || []; /* TODO */ - this.pul = false; /* TODO */ + this.pul = config.pul || false; this.displayName = config.name; this.time = config.time || 0; - this.UUID = UUIDGen.generate(sprintf("blind-%s",config.address)); + this.UUID = UUIDGen.generate(sprintf("blind-%s", config.address)); this.log = log; - + this.invert = config.invert || false; + this.runningStartTime = -1; + this.startPosition = 0; this.runningDirection = Characteristic.PositionState.STOPPED; this.state = Characteristic.PositionState.STOPPED; this.currentPosition = 0; this.targetPosition = 0; this.startDelayMs = config.startDelayMs || 0; /* Start delay of the automation and MH relay */ - this.timeAdjust = config.timeAdjust || 5; /* Percent error, F411 is a bit buggy */ + this.timeAdjust = config.timeAdjust || 0; /* Percent error, F411 is a bit buggy */ this.log.info(sprintf("LegrandMyHome::MHBlind create object: %s", this.address)); + + var address = this.address.split("/"); + this.bus = parseInt(address[0]); + this.ambient = parseInt(address[1]); + this.pl = parseInt(address[2]); + this.timer = 0; + this.updateTimer = 0; + this.travelTimeMs = -1; } evaluatePosition() { - if (this.runningDirection == Characteristic.PositionState.STOPPED && this.currentPosition != Characteristic.PositionState.STOPPED) { + if (this.runningDirection == Characteristic.PositionState.STOPPED && this.state != Characteristic.PositionState.STOPPED) { this.runningStartTime = new Date(); + this.startPosition = this.currentPosition; this.runningDirection = this.state; - this.log.debug(sprintf("Starting position is %d", this.currentPosition)) + this.log.debug(sprintf("Starting position is %d", this.currentPosition)); + /* Use the calculated travel time only if the target isn't the complete Up or Complete Down */ + if (this.travelTimeMs > 0) { + clearTimeout(this.timer); + this.timer = setTimeout(function () { + this.mh.simpleBlindCommand(this.address, 0); + this.log.debug(sprintf("setTargetPosition %s at %s done", this.address, this.targetPosition)); + }.bind(this), this.travelTimeMs); + } } else { if (this.runningDirection != Characteristic.PositionState.STOPPED && this.state == Characteristic.PositionState.STOPPED) { if (this.runningDirection == Characteristic.PositionState.INCREASING) { - this.currentPosition = Math.min(100,this.currentPosition + (100 / (this.time*1000) * (((new Date())-this.runningStartTime+this.startDelayMs)*(1+this.timeAdjust/100)))) - } else { - this.currentPosition = Math.max(0,this.currentPosition - (100 / (this.time*1000) * (((new Date())-this.runningStartTime+this.startDelayMs)*(1+this.timeAdjust/100)))) + this.currentPosition = Math.min(100, this.startPosition + (100 / (this.time * 1000) * (((new Date()) - this.runningStartTime + this.startDelayMs) * (1 + this.timeAdjust / 100)))); + } + if (this.runningDirection == Characteristic.PositionState.DECREASING) { + this.currentPosition = Math.max(0, this.startPosition - (100 / (this.time * 1000) * (((new Date()) - this.runningStartTime + this.startDelayMs) * (1 + this.timeAdjust / 100)))); } this.runningDirection = this.state; this.targetPosition = this.currentPosition; this.runningStartTime = -1; + this.travelTimeMs = -1; - this.log.debug(sprintf("Ending position is %d", this.currentPosition)) + this.log.debug(sprintf("Ending position is %d", this.currentPosition)); } else { - /* Uhm... */ + if (this.runningDirection == Characteristic.PositionState.INCREASING) { + this.currentPosition = Math.min(100, this.startPosition + (100 / (this.time * 1000) * (((new Date()) - this.runningStartTime + this.startDelayMs) * (1 + this.timeAdjust / 100)))); + } + if (this.runningDirection == Characteristic.PositionState.DECREASING) { + this.currentPosition = Math.max(0, this.startPosition - (100 / (this.time * 1000) * (((new Date()) - this.runningStartTime + this.startDelayMs) * (1 + this.timeAdjust / 100)))); + } } } } /* Calc the needed time to go from Max to Min */ - evaluateTravelTimeMs(from,to) { + evaluateTravelTimeMs(from, to) { if (this.time == 0) return -1; - return ((this.time*1000) / 100 * Math.abs(from-to)); + return ((this.time * 1000) / 100 * Math.abs(from - to)); } getServices() { var service = new Service.AccessoryInformation(); service.setCharacteristic(Characteristic.Name, this.name) - .setCharacteristic(Characteristic.Manufacturer, "Legrand MyHome") + .setCharacteristic(Characteristic.Manufacturer, "Simone Tisa") .setCharacteristic(Characteristic.Model, "Blind") + .setCharacteristic(Characteristic.FirmwareRevision, version) .setCharacteristic(Characteristic.SerialNumber, "Address " + this.address); this.windowCoveringService = new Service.WindowCovering(this.name); this.windowCoveringService.getCharacteristic(Characteristic.PositionState) .on('get', (callback) => { - this.log.debug(sprintf("getPositionState %s = %s",this.address, this.state)); + this.log.debug(sprintf("getPositionState %s = %s", this.address, this.state)); callback(null, this.state); }); this.windowCoveringService.getCharacteristic(Characteristic.CurrentPosition) .on('get', (callback) => { - this.log.debug(sprintf("getCurrentPosition %s = %s",this.address, this.state)); + this.log.debug(sprintf("getCurrentPosition %s = %s", this.address, this.currentPosition)); callback(null, this.currentPosition); - }); + }); this.windowCoveringService.getCharacteristic(Characteristic.TargetPosition) .on('set', (value, callback) => { this.targetPosition = value; - var travelTimeMs = this.evaluateTravelTimeMs(this.currentPosition,this.targetPosition); - if (value > this.currentPosition) { - this.mh.simpleBlindCommand(this.address,1); - } else { - this.mh.simpleBlindCommand(this.address,2); - } + this.log.debug(sprintf("setTargetPosition %s = %s", this.address, this.targetPosition)); + + + if (this.invert == false) + if (value > this.currentPosition && this.state != Characteristic.PositionState.INCREASING) { + this.mh.simpleBlindCommand(this.address, 1); + } else if (value < this.currentPosition && this.state != Characteristic.PositionState.DECREASING) { + this.mh.simpleBlindCommand(this.address, 2); + } else { + this.mh.simpleBlindCommand(this.address, 0); + } + else + if (value > this.currentPosition && this.state != Characteristic.PositionState.INCREASING) { + this.mh.simpleBlindCommand(this.address, 2); + } else if (value < this.currentPosition && this.state != Characteristic.PositionState.DECREASING) { + this.mh.simpleBlindCommand(this.address, 1); + } else { + this.mh.simpleBlindCommand(this.address, 0); + } /* Use the calculated travel time only if the target isn't the complete Up or Complete Down */ - if (this.targetPosition > 0 && this.targetPosition < 100) { - if (travelTimeMs > 0) { - setTimeout(function() { - this.mh.simpleBlindCommand(this.address,0); - }.bind(this), travelTimeMs); - } - } + if (this.targetPosition > 0 && this.targetPosition < 100 && this.currentPosition != this.targetPosition) + this.travelTimeMs = this.evaluateTravelTimeMs(this.currentPosition, this.targetPosition); + else + this.travelTimeMs = -1; + callback(null); }) .on('get', (callback) => { - this.log.debug(sprintf("getTargetPosition %s = %s",this.address, this.state)); + this.log.debug(sprintf("getTargetPosition %s = %s", this.address, this.targetPosition)); callback(null, this.targetPosition); }); @@ -385,9 +1170,9 @@ class MHBlindAdvanced { this.pul = false; /* TODO */ this.displayName = config.name; //this.time = config.time || 0; - this.UUID = UUIDGen.generate(sprintf("blindplus-%s",config.address)); + this.UUID = UUIDGen.generate(sprintf("blindplus-%s", config.address)); this.log = log; - + this.state = Characteristic.PositionState.STOPPED; this.currentPosition = 0; this.targetPosition = 0; @@ -397,21 +1182,22 @@ class MHBlindAdvanced { getServices() { var service = new Service.AccessoryInformation(); service.setCharacteristic(Characteristic.Name, this.name) - .setCharacteristic(Characteristic.Manufacturer, "Legrand MyHome") + .setCharacteristic(Characteristic.Manufacturer, "Simone Tisa") .setCharacteristic(Characteristic.Model, "Advanced Blind") + .setCharacteristic(Characteristic.FirmwareRevision, version) .setCharacteristic(Characteristic.SerialNumber, "Address " + this.address); this.windowCoveringPlusService = new Service.WindowCovering(this.name); this.windowCoveringPlusService.getCharacteristic(Characteristic.PositionState) .on('get', (callback) => { - this.log.info(sprintf("getPositionState %s = %s",this.address, this.state)); + this.log.debug(sprintf("getPositionState %s = %s", this.address, this.state)); callback(null, this.state); }); this.windowCoveringPlusService.getCharacteristic(Characteristic.CurrentPosition) .on('get', (callback) => { - this.log.debug(sprintf("getCurrentPosition %s = %s",this.address, this.state)); + this.log.debug(sprintf("getCurrentPosition %s = %s", this.address, this.state)); callback(null, this.currentPosition); }); @@ -425,12 +1211,12 @@ class MHBlindAdvanced { } else { this.state = Characteristic.PositionState.DECREASING; } - this.mh.advancedBlindCommand(this.address,this.targetPosition); + this.mh.advancedBlindCommand(this.address, this.targetPosition); this.windowCoveringPlusService.getCharacteristic(Characteristic.PositionState).getValue(null); callback(null); }) .on('get', (callback) => { - this.log.debug(sprintf("getTargetPosition %s = %s",this.address, this.targetPosition)); + this.log.debug(sprintf("getTargetPosition %s = %s", this.address, this.targetPosition)); callback(null, this.targetPosition); }); @@ -445,54 +1231,60 @@ class MHDimmer { this.name = config.name; this.address = config.address; this.groups = config.groups || []; /* TODO */ - this.pul = false; /* TODO */ + this.pul = config.pul || false; this.displayName = config.name; - this.UUID = UUIDGen.generate(sprintf("dimmer-%s",config.address)); + this.UUID = UUIDGen.generate(sprintf("dimmer-%s", config.address)); this.log = log; - + this.power = false; this.bri = 100; this.sat = 0; this.hue = 0; this.log.info(sprintf("LegrandMyHome::MHRelay create object: %s", this.address)); + + var address = this.address.split("/"); + this.bus = parseInt(address[0]); + this.ambient = parseInt(address[1]); + this.pl = parseInt(address[2]); } getServices() { var service = new Service.AccessoryInformation(); service.setCharacteristic(Characteristic.Name, this.name) - .setCharacteristic(Characteristic.Manufacturer, "Legrand MyHome") + .setCharacteristic(Characteristic.Manufacturer, "Simone Tisa") .setCharacteristic(Characteristic.Model, "Dimmer") + .setCharacteristic(Characteristic.FirmwareRevision, version) .setCharacteristic(Characteristic.SerialNumber, "Address " + this.address); this.lightBulbService = new Service.Lightbulb(this.name); this.lightBulbService.getCharacteristic(Characteristic.On) .on('set', (level, callback) => { - this.log.debug(sprintf("setPower %s = %s",this.address, level)); + this.log.debug(sprintf("setPower %s = %s", this.address, level)); this.power = (level > 0); if (this.power && this.bri == 0) { this.bri = 100; } - this.mh.relayCommand(this.address,this.power) + this.mh.relayCommand(this.address, this.power); callback(null); }) .on('get', (callback) => { - this.log.debug(sprintf("getPower %s = %s",this.address, this.power)); + this.log.debug(sprintf("getPower %s = %s", this.address, this.power)); callback(null, this.power); }); this.lightBulbService.getCharacteristic(Characteristic.Brightness) .on('set', (level, callback) => { - this.log.debug(sprintf("setBrightness %s = %d",this.address, level)); + this.log.debug(sprintf("setBrightness %s = %d", this.address, level)); this.bri = parseInt(level); this.power = (this.bri > 0); - this.mh.dimmerCommand(this.address,this.bri) + this.mh.dimmerCommand(this.address, this.bri); callback(null); }) .on('get', (callback) => { - this.log(sprintf("getBrightness %s = %d",this.address, this.bri)); + this.log.debug(sprintf("getBrightness %s = %d", this.address, this.bri)); callback(null, this.bri); - }); + }); return [service, this.lightBulbService]; } } @@ -505,9 +1297,9 @@ class MHThermostat { this.name = config.name; this.address = config.address; this.displayName = config.name; - this.UUID = UUIDGen.generate(sprintf("thermostat-%s",config.address)); + this.UUID = UUIDGen.generate(sprintf("thermostat-%s", config.address)); this.log = log; - + this.ambient = 0; this.setpoint = 20; this.mode = -1; @@ -518,61 +1310,62 @@ class MHThermostat { getServices() { var service = new Service.AccessoryInformation(); service.setCharacteristic(Characteristic.Name, this.name) - .setCharacteristic(Characteristic.Manufacturer, "Legrand MyHome") + .setCharacteristic(Characteristic.Manufacturer, "Simone Tisa") .setCharacteristic(Characteristic.Model, "Thermostat") + .setCharacteristic(Characteristic.FirmwareRevision, version) .setCharacteristic(Characteristic.SerialNumber, "Address " + this.address); this.thermostatService = new Service.Thermostat(this.name); - this.thermostatService.getCharacteristic(Characteristic.CurrentTemperature).setProps({minValue: -50, minStep: 0.1, maxValue: 50}) + this.thermostatService.getCharacteristic(Characteristic.CurrentTemperature).setProps({ minValue: -50, minStep: 0.1, maxValue: 50 }) .on('get', (callback) => { - this.log.debug(sprintf("getCurrentTemperature %s = %s",this.address, this.ambient)); + this.log.debug(sprintf("getCurrentTemperature %s = %s", this.address, this.ambient)); callback(null, this.ambient); }); this.thermostatService.getCharacteristic(Characteristic.CurrentHeatingCoolingState) .on('get', (callback) => { - this.log.debug(sprintf("getCurrentHeatingCoolingState %s = %s",this.address, this.state)); + this.log.debug(sprintf("getCurrentHeatingCoolingState %s = %s", this.address, this.state)); callback(null, this.state); - }).on('set', (value,callback) => { + }).on('set', (value, callback) => { callback(null); - }); + }); this.thermostatService.getCharacteristic(Characteristic.TargetHeatingCoolingState) .on('get', (callback) => { - this.log.debug(sprintf("getTargetHeatingCoolingState %s = %s",this.address, this.state)); - if (parseInt(this.setpoint,10) > parseInt(this.ambient,10)) { + this.log.debug(sprintf("getTargetHeatingCoolingState %s = %s", this.address, this.state)); + if (parseInt(this.setpoint, 10) > parseInt(this.ambient, 10)) { callback(null, Characteristic.TargetHeatingCoolingState.HEAT); - } else if (parseInt(this.setpoint,10) < parseInt(this.ambient,10)) { + } else if (parseInt(this.setpoint, 10) < parseInt(this.ambient, 10)) { callback(null, Characteristic.TargetHeatingCoolingState.COOL); } else { callback(null, Characteristic.TargetHeatingCoolingState.OFF); } - }).on('set', (value,callback) => { + }).on('set', (value, callback) => { this.state = value; - this.log.debug(sprintf("setTargetHeatingCoolingState %s = %s",this.address, this.state)); + this.log.debug(sprintf("setTargetHeatingCoolingState %s = %s", this.address, this.state)); callback(null); - }); + }); - this.thermostatService.getCharacteristic(Characteristic.TargetTemperature).setProps({minValue: 15, minStep:0.5, maxValue: 40}) + this.thermostatService.getCharacteristic(Characteristic.TargetTemperature).setProps({ minValue: 15, minStep: 0.5, maxValue: 40 }) .on('set', (value, callback) => { - this.log.debug(sprintf("setCurrentSetpoint %s = %s",this.address, value)); - this.mh.setSetPoint(this.address,value); + this.log.debug(sprintf("setCurrentSetpoint %s = %s", this.address, value)); + this.mh.setSetPoint(this.address, value); callback(null); }).on('get', (callback) => { - this.log.debug(sprintf("getCurrentSetpoint %s = %s",this.address, this.setpoint)); + this.log.debug(sprintf("getCurrentSetpoint %s = %s", this.address, this.setpoint)); callback(null, this.setpoint); }); this.thermostatService.getCharacteristic(Characteristic.TemperatureDisplayUnits) - .on('set', (value,callback) => { + .on('set', (value, callback) => { callback(null); }).on('get', (callback) => { - this.log.debug(sprintf("getTemperatureDisplayUnits %s = %s",this.address, this.ambient)); + this.log.debug(sprintf("getTemperatureDisplayUnits %s = %s", this.address, this.ambient)); callback(null, Characteristic.TemperatureDisplayUnits.CELSIUS); }); return [service, this.thermostatService]; - } + } } class MHThermometer { @@ -582,9 +1375,9 @@ class MHThermometer { this.name = config.name; this.address = config.address; this.displayName = config.name; - this.UUID = UUIDGen.generate(sprintf("thermometer-%s",config.address)); + this.UUID = UUIDGen.generate(sprintf("thermometer-%s", config.address)); this.log = log; - + this.ambient = -1; this.log.info(sprintf("LegrandMyHome::MHThermometer create object: %s", this.address)); } @@ -592,115 +1385,798 @@ class MHThermometer { getServices() { var service = new Service.AccessoryInformation(); service.setCharacteristic(Characteristic.Name, this.name) - .setCharacteristic(Characteristic.Manufacturer, "Legrand MyHome") + .setCharacteristic(Characteristic.Manufacturer, "Simone Tisa") .setCharacteristic(Characteristic.Model, "Thermometer") + .setCharacteristic(Characteristic.FirmwareRevision, version) .setCharacteristic(Characteristic.SerialNumber, "Address " + this.address); this.thermometerService = new Service.TemperatureSensor(this.name); - this.thermometerService.getCharacteristic(Characteristic.CurrentTemperature).setProps({minValue: -50, minStep: 0.1, maxValue: 50}) + this.thermometerService.getCharacteristic(Characteristic.CurrentTemperature).setProps({ minValue: -50, minStep: 0.1, maxValue: 50 }) .on('get', (callback) => { - this.log.debug(sprintf("getCurrentTemperature %s = %s",this.address, this.ambient)); + this.log.debug(sprintf("getCurrentTemperature %s = %s", this.address, this.ambient)); callback(null, this.ambient); }); return [service, this.thermometerService]; - } + } +} + +class MHPowerMeter { + constructor(log, config) { + this.config = config || {}; + this.mh = config.parent.controller; + this.name = config.name; + this.displayName = config.name; + this.UUID = UUIDGen.generate(sprintf("powermeter-%s", config.address)); + this.log = log; + this.refresh = config.refresh || 15; + this.intPower = 0; + this.acquiredSamples = 0; + this.lastReset = 0; + this.value = 0; + this.totalenergy = 0; + this.totalenergytemp = 0; + this.ExtraPersistedData = {}; + this.log.info(sprintf("LegrandMyHome::MHPowerMeter create object")); + correctingInterval.setCorrectingInterval(function () { + if (this.powerLoggingService.isHistoryLoaded()) { + this.ExtraPersistedData = this.powerLoggingService.getExtraPersistedData(); + if (this.ExtraPersistedData != undefined) { + this.totalenergy = this.ExtraPersistedData.totalenergy + this.totalenergytemp + this.value * this.refresh / 3600 / 1000; + this.powerLoggingService.setExtraPersistedData({ totalenergy: this.totalenergy, lastReset: this.ExtraPersistedData.lastReset }); + } + else { + this.totalenergy = this.totalenergytemp + this.value * this.refresh / 3600 / 1000; + this.powerLoggingService.setExtraPersistedData({ totalenergy: this.totalenergy, lastReset: 0 }); + } + this.totalenergytemp = 0; + + } + else { + this.totalenergytemp = this.totalenergytemp + this.value * this.refresh / 3600 / 1000; + this.totalenergy = this.totalenergytemp; + } + this.powerMeterService.getCharacteristic(LegrandMyHome.CurrentPowerConsumption).getValue(null); + this.powerMeterService.getCharacteristic(LegrandMyHome.TotalConsumption).getValue(null); + this.powerLoggingService.addEntry({ time: moment().unix(), power: this.value }); + this.mh.getPower(); + }.bind(this), this.refresh * 1000); + } + + identify(callback) { + this.log("Identify requested!"); + callback(); // success + } + + getServices() { + var service = new Service.AccessoryInformation(); + + service.setCharacteristic(Characteristic.Name, this.name) + .setCharacteristic(Characteristic.Manufacturer, "Simone Tisa") + .setCharacteristic(Characteristic.Model, "Power Meter") + .setCharacteristic(Characteristic.FirmwareRevision, version) + .setCharacteristic(Characteristic.SerialNumber, "Name-" + this.name); + + this.outlet = new Service.Outlet(this.name + " dumb switch"); + + this.powerMeterService = new LegrandMyHome.PowerMeterService(this.name); + this.powerMeterService.getCharacteristic(LegrandMyHome.CurrentPowerConsumption) + .on('get', (callback) => { + this.log.debug(sprintf("getConsumptio = %s", this.value)); + callback(null, this.value); + }); + this.powerMeterService.getCharacteristic(LegrandMyHome.TotalConsumption) + .on('get', (callback) => { + this.ExtraPersistedData = this.powerLoggingService.getExtraPersistedData(); + if (this.ExtraPersistedData != undefined) + this.totalenergy = this.ExtraPersistedData.totalenergy; + this.log.debug(sprintf("getConsumptio = %f", this.totalenergy)); + callback(null, this.totalenergy); + }); + this.powerMeterService.getCharacteristic(LegrandMyHome.ResetTotal) + .on('set', (value, callback) => { + this.totalenergy = 0; + this.lastReset = value; + this.powerLoggingService.setExtraPersistedData({ totalenergy: this.totalenergy, lastReset: this.lastReset }); + callback(null); + }) + .on('get', (callback) => { + this.ExtraPersistedData = this.powerLoggingService.getExtraPersistedData(); + if (this.ExtraPersistedData != undefined) + this.lastReset = this.ExtraPersistedData.lastReset; + callback(null, this.lastReset); + }); + + if (this.config.storage == 'fs') + this.powerLoggingService = new LegrandMyHome.FakeGatoHistoryService("energy", this, { storage: 'fs' }); + else + this.powerLoggingService = new LegrandMyHome.FakeGatoHistoryService("energy", this, { storage: 'googleDrive', path: 'homebridge' }); + + return [service, this.powerMeterService, this.powerLoggingService, this.outlet]; + } } -class MHContactSensor { +class MHButton { constructor(log, config) { this.config = config || {}; this.mh = config.parent.controller; this.name = config.name; this.address = config.address; this.displayName = config.name; - this.UUID = UUIDGen.generate(sprintf("contactsensor-%s",config.address)); + this.UUID = UUIDGen.generate(sprintf("button-%s", config.address)); this.log = log; - - this.state = false; - this.log.info(sprintf("LegrandMyHome::MHContactSensor create object: %s", this.address)); + + this.value = 0; + this.log.info(sprintf("LegrandMyHome::MHButton (CEN/CEN+) create object: %s", this.address)); } getServices() { var service = new Service.AccessoryInformation(); service.setCharacteristic(Characteristic.Name, this.name) - .setCharacteristic(Characteristic.Manufacturer, "Legrand MyHome") - .setCharacteristic(Characteristic.Model, "Contact Sensor") + .setCharacteristic(Characteristic.Manufacturer, "Simone Tisa") + .setCharacteristic(Characteristic.Model, "CEN/CEN+") + .setCharacteristic(Characteristic.FirmwareRevision, version) .setCharacteristic(Characteristic.SerialNumber, "Address " + this.address); - this.contactSensorService = new Service.ContactSensor(this.name); - this.contactSensorService.getCharacteristic(Characteristic.ContactSensorState) + this.statelessSwitch = new Service.Switch(this.name); + this.statelessSwitch.getCharacteristic(Characteristic.On) + .on('set', (value, callback) => { + this.log.debug(sprintf("setOn %s = %s", this.address, value)); + callback(null); + }) .on('get', (callback) => { - this.log.debug(sprintf("getContactSensorState %s = %s",this.address, this.state)); - callback(null, this.state); + this.log.debug(sprintf("getOn %s", this.address)); + callback(null, 0); }); + return [service, this.statelessSwitch]; + } - return [service, this.contactSensorService]; - } } -class MHPowerMeter { +class MHScenario { constructor(log, config) { this.config = config || {}; this.mh = config.parent.controller; this.name = config.name; this.address = config.address; this.displayName = config.name; - this.UUID = UUIDGen.generate(sprintf("powermeter-%s",config.address)); + this.UUID = UUIDGen.generate(sprintf("scenario-%s", config.address)); this.log = log; - - this.value = 0; - this.log.info(sprintf("LegrandMyHome::MHPowerMeter create object: %s", this.address)); + + this.state = 0; + this.running = 0; + this.log.info(sprintf("LegrandMyHome::MHScenario create object: %s", this.address)); } getServices() { var service = new Service.AccessoryInformation(); service.setCharacteristic(Characteristic.Name, this.name) - .setCharacteristic(Characteristic.Manufacturer, "Legrand MyHome") - .setCharacteristic(Characteristic.Model, "Power Meter") + .setCharacteristic(Characteristic.Manufacturer, "Simone Tisa") + .setCharacteristic(Characteristic.Model, "Scenario") + .setCharacteristic(Characteristic.FirmwareRevision, version) .setCharacteristic(Characteristic.SerialNumber, "Address " + this.address); - this.powerMeterService = new LegrandMyHome.PowerMeterService(this.name); - this.powerMeterService.getCharacteristic(LegrandMyHome.CurrentPowerConsumption) + this.scenarioService = new LegrandMyHome.ScenarioService(this.name); + this.scenarioService.getCharacteristic(Characteristic.Active) + .on('set', (value, callback) => { + this.state = value; + this.mh.enableScenarioCommand(this.address, this.state); + this.log.debug(sprintf("setOn %s = %s", this.address, this.state)); + callback(null); + }) .on('get', (callback) => { - this.log.info(sprintf("getConsumption %s = %s",this.address, this.state)); - callback(null, this.value); + this.log.debug(sprintf("getOn %s", this.address)); + callback(null, this.state); + }); + this.scenarioService.getCharacteristic(LegrandMyHome.SimpleBoolean) + .on('set', (value, callback) => { + this.running = value; + this.mh.runScenarioCommand(this.address, this.running); + this.log.debug(sprintf("runOn %s = %s", this.address, this.state)); + callback(null); + }) + .on('get', (callback) => { + this.log.debug(sprintf("getOn %s", this.address)); + callback(null, this.running); }); - return [service, this.powerMeterService]; - } + return [service, this.scenarioService]; + } + } -class MHButton { + +class MHDryContact { constructor(log, config) { this.config = config || {}; this.mh = config.parent.controller; this.name = config.name; this.address = config.address; this.displayName = config.name; - this.UUID = UUIDGen.generate(sprintf("button-%s",config.address)); + this.UUID = UUIDGen.generate(sprintf("drycontact-%s", config.address)); this.log = log; - - this.value = 0; - this.log.info(sprintf("LegrandMyHome::MHButton (CEN/CEN+) create object: %s", this.address)); + this.type = config.type; + this.numberOpened = 0; + this.durationhandle = null; + this.duration = config.duration || 30; + this.firstGet = true; + this.lastOpening = 0; + this.lastActivation = 0; + this.state = config.state || false; + this.ExtraPersistedData = {}; + this.log.info(sprintf("LegrandMyHome::MHDryContact create object: %s", this.address)); + } + + identify(callback) { + this.log("Identify requested!"); + callback(); // success } getServices() { var service = new Service.AccessoryInformation(); service.setCharacteristic(Characteristic.Name, this.name) - .setCharacteristic(Characteristic.Manufacturer, "Legrand MyHome") - .setCharacteristic(Characteristic.Model, "CEN/CEN+") + .setCharacteristic(Characteristic.Manufacturer, "Simone Tisa") + .setCharacteristic(Characteristic.Model, "Dry-contact Sensor") + .setCharacteristic(Characteristic.FirmwareRevision, version) .setCharacteristic(Characteristic.SerialNumber, "Address " + this.address); - this.statelessSwitch = new Service.Switch(this.name); - this.statelessSwitch.getCharacteristic(Characteristic.On) - .on('set', (value,callback) => { - this.log.info(sprintf("setOn %s = %s",this.address, value)); + switch (this.type) { + case 'Contact': + this.dryContactService = new Service.ContactSensor(this.name); + if (this.config.storage == 'fs') + this.LoggingService = new LegrandMyHome.FakeGatoHistoryService("door", this, { storage: 'fs' }); + else + this.LoggingService = new LegrandMyHome.FakeGatoHistoryService("door", this, { storage: 'googleDrive', path: 'homebridge' }); + this.dryContactService.addCharacteristic(LegrandMyHome.LastActivation); + this.dryContactService.addCharacteristic(LegrandMyHome.TimesOpened); + this.dryContactService.addCharacteristic(LegrandMyHome.ResetTotal); + this.dryContactService.addCharacteristic(LegrandMyHome.Char118); + this.dryContactService.addCharacteristic(LegrandMyHome.Char119); + this.ExtraPersistedData = this.LoggingService.getExtraPersistedData(); + if (this.ExtraPersistedData != undefined) { + this.numberOpened = this.ExtraPersistedData.numberOpened || 0; + this.lastOpening = this.ExtraPersistedData.lastOpening || 0; + this.lastReset = this.ExtraPersistedData.lastReset || 0; + } + + this.dryContactService.getCharacteristic(Characteristic.ContactSensorState) + .on('get', (callback) => { + this.log.debug(sprintf("getContactSensorState %s = %s", this.address, this.state)); + if (this.firstGet) { + this.firstGet = false; + this.LoggingService.addEntry({ time: moment().unix(), status: this.state }); + } + callback(null, this.state); + }); + this.dryContactService.getCharacteristic(Characteristic.ContactSensorState) + .on('change', () => { + this.log.debug(sprintf("changeContactSensorState %s = %s", this.address, this.state)); + this.ExtraPersistedData = this.LoggingService.getExtraPersistedData(); + if (this.ExtraPersistedData != undefined && this.ExtraPersistedData.lastReset != undefined) + { + this.lastReset = this.ExtraPersistedData.lastReset || 0; + } + if (this.ExtraPersistedData != undefined && this.ExtraPersistedData.numberOpened != undefined) + { + this.numberOpened = this.ExtraPersistedData.numberOpened; + } + this.lastOpening = moment().unix() - this.LoggingService.getInitialTime(); + if (this.state) { + + if (this.ExtraPersistedData != undefined && this.ExtraPersistedData.numberOpened != undefined) + this.numberOpened = this.ExtraPersistedData.numberOpened + 1; + else + this.numberOpened++; + } + this.LoggingService.setExtraPersistedData({ numberOpened: this.numberOpened, lastOpening: this.lastOpening, lastReset: this.lastReset }); + this.LoggingService.addEntry({ time: moment().unix(), status: this.state }); + }); + this.dryContactService.getCharacteristic(LegrandMyHome.ResetTotal) + .on('set', (value, callback) => { + this.ExtraPersistedData = this.LoggingService.getExtraPersistedData(); + if (this.ExtraPersistedData != undefined && this.ExtraPersistedData.lastOpening != undefined) + { + this.lastOpening = this.ExtraPersistedData.lastOpening; + } + this.numberOpened = 0; + this.lastReset = value; + this.LoggingService.setExtraPersistedData({ numberOpened: this.numberOpened, lastOpening: this.lastOpening, lastReset: this.lastReset }); + this.LoggingService.addEntry({ time: moment().unix(), status: this.state }); + callback(null); + }) + .on('get', (callback) => { + this.ExtraPersistedData = this.LoggingService.getExtraPersistedData(); + if (this.ExtraPersistedData != undefined) + this.lastReset = this.ExtraPersistedData.lastReset; + callback(null, this.lastReset); + }); + this.dryContactService.getCharacteristic(LegrandMyHome.TimesOpened) + .on('get', (callback) => { + this.ExtraPersistedData = this.LoggingService.getExtraPersistedData(); + if (this.ExtraPersistedData != undefined) + this.numberOpened = this.ExtraPersistedData.numberOpened; + this.log.debug(sprintf("getNumberOpened = %f", this.numberOpened)); + callback(null, this.numberOpened); + }); + this.dryContactService.getCharacteristic(LegrandMyHome.LastActivation) + .on('get', (callback) => { + this.ExtraPersistedData = this.LoggingService.getExtraPersistedData(); + if (this.ExtraPersistedData != undefined) + this.lastOpening = this.ExtraPersistedData.lastOpening; + this.log.debug(sprintf("lastOpening = %f", this.lastOpening)); + callback(null, this.lastOpening); + }); + return [service, this.dryContactService, this.LoggingService]; + case 'Leak': + this.dryContactService = new Service.LeakSensor(this.name); + this.dryContactService.getCharacteristic(Characteristic.LeakDetected) + .on('get', (callback) => { + this.log.debug(sprintf("getLeakSensorState %s = %s", this.address, this.state)); + callback(null, this.state); + }); + return [service, this.dryContactService]; + case 'Motion': + this.dryContactService = new Service.MotionSensor(this.name); + if (this.config.storage == 'fs') + this.LoggingService = new LegrandMyHome.FakeGatoHistoryService("motion", this, { storage: 'fs' }); + else + this.LoggingService = new LegrandMyHome.FakeGatoHistoryService("motion", this, { storage: 'googleDrive', path: 'homebridge' }); + this.dryContactService.addCharacteristic(LegrandMyHome.Sensitivity); + this.dryContactService.addCharacteristic(LegrandMyHome.Duration); + this.dryContactService.addCharacteristic(LegrandMyHome.LastActivation); + this.dryContactService.setCharacteristic(LegrandMyHome.Duration, this.duration); + this.ExtraPersistedData = this.LoggingService.getExtraPersistedData(); + if (this.ExtraPersistedData != undefined) { + this.lastActivation = this.ExtraPersistedData.lastActivation || 0; + } + this.dryContactService.getCharacteristic(Characteristic.MotionDetected) + .on('get', (callback) => { + this.log.debug(sprintf("getMotionSensorState %s = %s", this.address, this.state)); + if (this.firstGet) { + this.firstGet = false; + this.LoggingService.addEntry({ time: moment().unix(), status: this.state }); + } + callback(null, this.state); + }); + this.dryContactService.getCharacteristic(Characteristic.MotionDetected) + .on('change', () => { + this.log.debug(sprintf("changeMotionSensorState %s = %s", this.address, this.state)); + this.lastActivation = moment().unix() - this.LoggingService.getInitialTime(); + this.LoggingService.setExtraPersistedData({ lastActivation: this.lastActivation }); + this.LoggingService.addEntry({ time: moment().unix(), status: this.state }); + + }); + this.dryContactService.getCharacteristic(LegrandMyHome.Duration) + .on('set', (value, callback) => { + this.duration = value; + callback(null); + }) + .on('get', (callback) => { + callback(null, this.duration); + }); + this.dryContactService.getCharacteristic(LegrandMyHome.LastActivation) + .on('get', (callback) => { + this.ExtraPersistedData = this.LoggingService.getExtraPersistedData(); + if (this.ExtraPersistedData != undefined) + this.lastActivation = this.ExtraPersistedData.lastActivation; + this.log.debug(sprintf("lastActivation = %f", this.lastActivation)); + callback(null, this.lastActivation); + }); + + return [service, this.dryContactService, this.LoggingService]; + default: + this.dryContactService = new Service.ContactSensor(this.name); + this.dryContactService.getCharacteristic(Characteristic.ContactSensorState) + .on('get', (callback) => { + this.log.debug(sprintf("getContactSensorState %s = %s", this.address, this.state)); + callback(null, this.state); + }); + return [service, this.dryContactService]; + } + + + } +} + +class MHAux { + constructor(log, config) { + this.config = config || {}; + this.mh = config.parent.controller; + this.name = config.name; + this.address = config.address; + this.displayName = config.name; + this.UUID = UUIDGen.generate(sprintf("aux-%s", config.address)); + this.log = log; + this.type = config.type; + + this.state = false; + this.log.info(sprintf("LegrandMyHome::MHAux create object: %s", this.address)); + } + + getServices() { + var service = new Service.AccessoryInformation(); + service.setCharacteristic(Characteristic.Name, this.name) + .setCharacteristic(Characteristic.Manufacturer, "Simone Tisa") + .setCharacteristic(Characteristic.Model, "AUX") + .setCharacteristic(Characteristic.FirmwareRevision, version) + .setCharacteristic(Characteristic.SerialNumber, "Address " + this.address); + + switch (this.type) { + case 'Contact': + this.AUXService = new Service.ContactSensor(this.name); + this.AUXService.getCharacteristic(Characteristic.ContactSensorState) + .on('get', (callback) => { + this.log.debug(sprintf("getContactSensorState %s = %s", this.address, this.state)); + callback(null, this.state); + }); + break; + case 'Leak': + this.AUXService = new Service.LeakSensor(this.name); + this.AUXService.getCharacteristic(Characteristic.LeakDetected) + .on('get', (callback) => { + this.log.debug(sprintf("getLeakSensorState %s = %s", this.address, this.state)); + callback(null, this.state); + }); + break; + case 'Motion': + this.AUXService = new Service.MotionSensor(this.name); + this.AUXService.getCharacteristic(Characteristic.MotionDetected) + .on('get', (callback) => { + this.log.debug(sprintf("getMotionSensorState %s = %s", this.address, this.state)); + callback(null, this.state); + }); + break; + case 'Gas': + this.AUXService = new Service.CarbonMonoxideSensor(this.name); + this.AUXService.getCharacteristic(Characteristic.CarbonMonoxideDetected) + .on('get', (callback) => { + this.log.debug(sprintf("getGasSensorState %s = %s", this.address, this.state)); + callback(null, this.state); + }); + break; + default: + this.AUXService = new Service.ContactSensor(this.name); + this.AUXService.getCharacteristic(Characteristic.ContactSensorState) + .on('get', (callback) => { + this.log.debug(sprintf("getContactSensorState %s = %s", this.address, this.state)); + callback(null, this.state); + }); + break; + } + + return [service, this.AUXService]; + } +} + +class MHAlarm { + constructor(log, config) { + this.config = config || {}; + this.mh = config.parent.controller; + this.name = config.name; + this.displayName = config.name; + this.UUID = UUIDGen.generate(sprintf("alarm-%s", config.address)); + this.log = log; + + this.state = Characteristic.SecuritySystemCurrentState.DISARMED; + this.target = Characteristic.SecuritySystemCurrentState.DISARMED; + this.fault = Characteristic.StatusFault.NO_FAULT; + this.tampered = Characteristic.StatusTampered.NOT_TAMPERED; + this.lowbattery = Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; + this.batterycharging = Characteristic.ChargingState.NOT_CHARGING; + this.batterylevel = 100; + this.zone = [true, true, true, true, false, false, false, false]; + this.active = false; + this.triggered = false; + + this.log.info(sprintf("LegrandMyHome::MHAlarm create object")); + + } + + getServices() { + var service = new Service.AccessoryInformation(); + service.setCharacteristic(Characteristic.Name, this.name) + .setCharacteristic(Characteristic.Manufacturer, "Simone Tisa") + .setCharacteristic(Characteristic.Model, "Alarm 3486") + .setCharacteristic(Characteristic.FirmwareRevision, version) + .setCharacteristic(Characteristic.SerialNumber, "Name- " + this.name); + + this.alarmService = new Service.SecuritySystem(this.name); + this.alarmService.getCharacteristic(Characteristic.SecuritySystemCurrentState) + .on('get', (callback) => { + this.log.debug(sprintf("alarm current = %d", this.state)); + callback(null, this.state); + }); + this.alarmService.getCharacteristic(Characteristic.SecuritySystemTargetState) + .on('get', (callback) => { + this.log.debug(sprintf("alarm get target = %d", this.target)); + callback(null, this.target); + }) + .on('set', (value, callback) => { + this.log.debug(sprintf("alarm set target= %d", this.target)); + callback(null); + }); + this.alarmService.getCharacteristic(Characteristic.StatusFault) + .on('get', (callback) => { + this.log.debug(sprintf("alarm status fault get = %s", this.fault)); + callback(null, this.fault); + }); + this.alarmService.getCharacteristic(Characteristic.StatusTampered) + .on('get', (callback) => { + this.log.debug(sprintf("alarm status tampered get = %s", this.tampered)); + callback(null, this.tampered); + }); + this.alarmBatteryService = new Service.BatteryService(this.name); + this.alarmBatteryService.getCharacteristic(Characteristic.StatusLowBattery) + .on('get', (callback) => { + this.log.debug(sprintf("alarm statuslowbatery = %d", this.lowbattery)); + callback(null, this.lowbattery); + }); + this.alarmBatteryService.getCharacteristic(Characteristic.BatteryLevel) + .on('get', (callback) => { + this.log.debug(sprintf("alarm batterylevel = %d", this.batterylevel)); + callback(null, this.batterylevel); + }); + this.alarmBatteryService.getCharacteristic(Characteristic.ChargingState) + .on('get', (callback) => { + this.log.debug(sprintf("alarm batterycharging = %d", this.batterycharging)); + callback(null, this.batterycharging); + }); + + + return [service, this.alarmService, this.alarmBatteryService]; + } +} + +class MHControlledLoad { + constructor(log, config) { + this.config = config || {}; + this.mh = config.parent.controller; + this.name = config.name; + this.address = config.address; + this.displayName = config.name; + this.UUID = UUIDGen.generate(sprintf("controlledload-%s", config.address)); + this.log = log; + + this.enabled = true; + this.forced = 0; + this.log.info(sprintf("LegrandMyHome::MHControlledLoad create priority: %s", this.address)); + } + + getServices() { + var service = new Service.AccessoryInformation(); + service.setCharacteristic(Characteristic.Name, this.name) + .setCharacteristic(Characteristic.Manufacturer, "Simone Tisa") + .setCharacteristic(Characteristic.Model, "Controlled load") + .setCharacteristic(Characteristic.FirmwareRevision, version) + .setCharacteristic(Characteristic.SerialNumber, "Priority " + this.address); + + this.controlledLoad = new LegrandMyHome.ControlledLoadService(this.name); + + this.controlledLoad.getCharacteristic(Characteristic.Active) + .on('set', (value, callback) => { + this.log.debug(sprintf("setOn %s = %s", this.address, value)); + + if (value == 1) { + this.enabled = true; + this.forced = value; + this.mh.forcedLoadCommand(this.address, this.forced); + callback(null); + } + else { + this.forced = 0; + this.controlledLoad.getCharacteristic(Characteristic.Active).getValue(null); + setTimeout(function () { + this.forced = 1; + this.controlledLoad.getCharacteristic(Characteristic.Active).getValue(null); + }.bind(this), 500); + callback(null); + } + + }) + .on('get', (callback) => { + this.log.debug(sprintf("getOn %s", this.address)); + callback(null, this.forced); + }); + this.controlledLoad.getCharacteristic(Characteristic.OutletInUse) + .on('get', (callback) => { + this.log.debug(sprintf("getOn %s", this.address)); + callback(null, this.enabled); + }); + return [service, this.controlledLoad]; + } + +} + +class MHIrrigation { + constructor(log, config) { + this.config = config || {}; + this.mh = config.parent.controller; + this.name = config.name; + this.address = config.address; + this.groups = config.groups || []; /* TODO */ + this.pul = config.pul || false; + this.displayName = config.name; + this.UUID = UUIDGen.generate(sprintf("irrigation-%s", config.address)); + this.log = log; + this.power = false; + this.RemDuration = 1; + this.timer = config.timer || 1; + this.timerHandle = 0; + this.log.info(sprintf("LegrandMyHome::MHIrrigation create object: %s", this.address)); + this.mh.addLightBusDevice(this.address); + var address = this.address.split("/"); + this.bus = parseInt(address[0]); + this.ambient = parseInt(address[1]); + this.pl = parseInt(address[2]); + this.lastActivation = 0; + this.ExtraPersistedData = {}; + this.firstGet = true; + this.data131 = ""; + this.waterFlux = 1000; + this.totalWaterAmount = 0; + this.currentWaterAmount = 0; + this.timeOpening = 0; + this.lastReset = 0; + } + + getServices() { + var service = new Service.AccessoryInformation(); + service.setCharacteristic(Characteristic.Name, this.name) + .setCharacteristic(Characteristic.Manufacturer, "Simone Tisa") + .setCharacteristic(Characteristic.Model, "Irrigation") + .setCharacteristic(Characteristic.FirmwareRevision, version) + .setCharacteristic(Characteristic.SerialNumber, "Address " + this.bus + "-" + this.ambient + "-" + this.pl); + + this.IrrigationService = new Service.Valve(this.name); + + if (this.config.storage == 'fs') + this.LoggingService = new LegrandMyHome.FakeGatoHistoryService("aqua", this, { storage: 'fs', disableTimer: true }); + else + this.LoggingService = new LegrandMyHome.FakeGatoHistoryService("aqua", this, { storage: 'googleDrive', path: 'homebridge', disableTimer: true }); + this.ExtraPersistedData = this.LoggingService.getExtraPersistedData(); + if (this.ExtraPersistedData != undefined) { + this.lastActivation = this.ExtraPersistedData.lastActivation || 0; + this.waterFlux = this.ExtraPersistedData.waterFlux || 1000; + this.totalWaterAmount = this.ExtraPersistedData.totalWaterAmount || 0; + this.lastReset = this.ExtraPersistedData.lastReset || 0; + } + + this.LoggingService.addCharacteristic(LegrandMyHome.Char11D); + this.LoggingService.addCharacteristic(LegrandMyHome.Char131); + this.IrrigationService.addCharacteristic(LegrandMyHome.ResetTotal); + this.IrrigationService.setCharacteristic(Characteristic.ValveType, 1); + this.IrrigationService.setCharacteristic(Characteristic.SetDuration, this.timer); + this.LoggingService.getCharacteristic(LegrandMyHome.Char131) + .on('get', (callback) => { + this.ExtraPersistedData = this.LoggingService.getExtraPersistedData(); + if (this.ExtraPersistedData != undefined) { + this.lastActivation = this.ExtraPersistedData.lastActivation || 0; + this.waterFlux = this.ExtraPersistedData.waterFlux || 1000; + this.totalWaterAmount = this.ExtraPersistedData.totalWaterAmount || 0; + this.lastReset = this.ExtraPersistedData.lastReset || 0; + } + this.data131 = Format("0002230003021b04040c4156323248314130303036330602080007042a3000000b0200000501000204f82c00001401030f0400000000450505000000004609050000000e000042064411051c0005033c0000003a814b42a34d8c4047110594186d19071ad91ab40000003c00000048060500000000004a06050000000000d004 %s 9b04 %s 2f0e %s 00000000 00000000 %s 2d06 0000000000001e02300c", + numToHex(swap32(this.lastActivation), 8), + numToHex(swap32(moment().unix() - this.LoggingService.getInitialTime()), 8), + numToHex(swap32(this.totalWaterAmount), 8), + numToHex(swap16(this.waterFlux), 4) + ); + this.log.debug("Data 131 %s: %s", this.name, this.data131); + callback(null, hexToBase64(this.data131)); + }); + this.LoggingService.getCharacteristic(LegrandMyHome.Char11D) + .on('set', (_value, callback) => { + var valHex = base64ToHex(_value); + this.log.debug("Data 11D %s: %s", this.name, valHex); + var substringCommand = valHex.substring(0, 4); + this.ExtraPersistedData = this.LoggingService.getExtraPersistedData(); + if (this.ExtraPersistedData != undefined) { + this.lastActivation = this.ExtraPersistedData.lastActivation || 0; + this.waterFlux = this.ExtraPersistedData.waterFlux || 1000; + this.totalWaterAmount = this.ExtraPersistedData.totalWaterAmount || 0; + this.lastReset = this.ExtraPersistedData.lastReset || 0; + } + if (substringCommand == "2e02") { //set waterFlux + var substringFlux = valHex.substring(4); + var valFluxInt = parseInt(substringFlux, 16); + this.waterFlux = swap16(valFluxInt); + } + + if (substringCommand == "2f04") { //reset totalizer + var substringLastReset = valHex.substring(4); + var valLastResetInt = parseInt(substringLastReset, 16); + this.lastReset = swap32(valLastResetInt); + this.totalWaterAmount = 0; + } + + this.LoggingService.setExtraPersistedData({ lastActivation: this.lastActivation, waterFlux: this.waterFlux, totalWaterAmount: this.totalWaterAmount, lastReset: this.lastReset }); + this.LoggingService.addEntry({ time: moment().unix(), status: this.power }); + this.log.debug("New flux %s: %s", this.name, this.waterFlux); + callback(null); + }); + this.IrrigationService.getCharacteristic(Characteristic.Active) + .on('set', (_value, callback) => { + this.log.debug(sprintf("setIrrigation %s = %s", this.address, _value)); + this.power = _value; + if (this.power) { + this.mh.relayTimedOn(this.address, this.timer / 3600, this.timer / 60, this.timer % 60); + } + else { + this.mh.relayCommand(this.address, this.power); + } + callback(null); }) .on('get', (callback) => { - this.log.info(sprintf("getOn %s",this.address)); - callback(null,0); + this.log.debug(sprintf("getIrrigation %s = %s", this.address, this.power)); + callback(null, this.power); }); - return [service, this.statelessSwitch]; - } + this.IrrigationService.getCharacteristic(Characteristic.InUse) + .on('get', (callback) => { + if (this.firstGet) { + this.firstGet = false; + this.LoggingService.addEntry({ time: moment().unix(), status: this.power }); + } + callback(null, this.power); + }) + .on('change', () => { + this.ExtraPersistedData = this.LoggingService.getExtraPersistedData(); + if (this.ExtraPersistedData != undefined) { + this.lastActivation = this.ExtraPersistedData.lastActivation || 0; + this.waterFlux = this.ExtraPersistedData.waterFlux || 1000; + this.totalWaterAmount = this.ExtraPersistedData.totalWaterAmount || 0; + this.lastReset = this.ExtraPersistedData.lastReset || 0; + } + if (this.power) { + setTimeout(function () { + this.mh.getRelayDuration(this.address); + }.bind(this), 1000); + this.timeOpening = moment().unix(); + } + else { + clearInterval(this.timerHandle); + if (moment().unix() - this.timeOpening > 0) { + this.currentWaterAmount = (moment().unix() - this.timeOpening) * this.waterFlux; + this.totalWaterAmount += this.currentWaterAmount; + } + else + this.currentWaterAmount = 0; + this.RemDuration = 0; + this.IrrigationService.setCharacteristic(Characteristic.RemainingDuration, this.RemDuration); + } + this.log.debug(sprintf("changeIrrigation %s = %s", this.address, this.power)); + this.lastActivation = moment().unix() - this.LoggingService.getInitialTime(); + + this.LoggingService.setExtraPersistedData({ lastActivation: this.lastActivation, waterFlux: this.waterFlux, totalWaterAmount: this.totalWaterAmount, lastReset: this.lastReset }); + this.LoggingService.addEntry({ time: moment().unix(), status: this.power, waterAmount: this.currentWaterAmount }); + }); + this.IrrigationService.getCharacteristic(Characteristic.SetDuration) + .on('set', (time, callback) => { + this.timer = time; + callback(null); + }) + .on('get', (callback) => { + callback(null, this.timer); + }); + this.IrrigationService.getCharacteristic(Characteristic.RemainingDuration) + .on('set', (time, callback) => { + this.RemDuration = time; + callback(null); + }) + .on('get', (callback) => { + callback(null, this.RemDuration); + }); + this.IrrigationService.getCharacteristic(LegrandMyHome.ResetTotal) + .on('get', (callback) => { + this.ExtraPersistedData = this.LoggingService.getExtraPersistedData(); + if (this.ExtraPersistedData != undefined) { + this.lastActivation = this.ExtraPersistedData.lastActivation || 0; + this.waterFlux = this.ExtraPersistedData.waterFlux || 1000; + this.totalWaterAmount = this.ExtraPersistedData.totalWaterAmount || 0; + this.lastReset = this.ExtraPersistedData.lastReset || 0; + } + callback(null, this.lastReset); + }); + + return [service, this.IrrigationService, this.LoggingService]; + } } \ No newline at end of file diff --git a/lib/mhclient.js b/lib/mhclient.js old mode 100644 new mode 100755 index 7d88e28..795ae76 --- a/lib/mhclient.js +++ b/lib/mhclient.js @@ -1,15 +1,16 @@ -const debug = require('debug')('myhomeclient') +/*jshint esversion: 6,node: true,-W041: false */ +const debug = require('debug')('myhomeclient'); const sprintf = require("sprintf-js").sprintf; const net = require('net'); const crypto = require('crypto'); -const sha256 = require('sha256'), sha1 = require('sha1'); - +const sha256 = require('sha256'); +const moment = require('moment'); function inArray(needle, haystack) { var length = haystack.length; - for(var i = 0; i < length; i++) { - if(haystack[i] == needle) + for (var i = 0; i < length; i++) { + if (haystack[i] == needle) return true; } return false; @@ -21,31 +22,32 @@ function inArray(needle, haystack) { class MyHomeClient { - constructor(_ipaddress, _port, _password, _classToCallback) { + constructor(_ipaddress, _port, _password, _setclock, _classToCallback) { // Ten level translation, 0=0=Off, 1 = 100=On, 2=1%, 3=10%, 4=20%...8=60%,9=75,10=100% - this.dimmerLevels = [0,100,1,10,20,30,40,50,60,75,100]; + this.dimmerLevels = [0, 100, 1, 10, 20, 30, 40, 50, 60, 75, 100]; this.buffers = {}; this.lightBuses = []; this.ipaddress = _ipaddress; this.password = _password; this.port = _port; + this.setclock = _setclock; this.parent = _classToCallback; } start() { - this.monitor = new MyHomeConnection(this.ipaddress, this.port, this.password, "MONITOR", null, this.onMonitor.bind(this)); - this.command = new MyHomeConnection(this.ipaddress, this.port, this.password, "COMMAND", this.onCommandConnect.bind(this), this.onCommand.bind(this)); + this.monitor = new MyHomeConnection(this.ipaddress, this.port, this.password, "MONITOR", null, this.onMonitor.bind(this), null); + this.command = new MyHomeConnection(this.ipaddress, this.port, this.password, "COMMAND", this.onCommandConnect.bind(this), this.onCommand.bind(this), this.setclock); } /** * Fill the this.lightBuses array to know how many bus(es) should be polled at the startup */ addLightBusDevice(_address) { - var address = _address.split("/"); + var address = _address.split("/"); if (address.length != 3) return ""; // Add this bus if it wasn't already in list - if (!inArray(address[0],this.lightBuses)) this.lightBuses.push(address[0]); + if (!inArray(address[0], this.lightBuses)) this.lightBuses.push(address[0]); } send(_command) { @@ -53,27 +55,33 @@ class MyHomeClient { } _slashesToAddress(_address) { - var address = _address.split("/"); + var address = _address.split("/"); if (address.length != 3) return ""; var b = parseInt(address[0]), a = parseInt(address[1]), pl = parseInt(address[2]); if (b == 0) { if (a >= 10 || pl >= 10) { - return sprintf("%02d%02d",a,pl); + return sprintf("%02d%02d", a, pl); } - return sprintf("%d%d",a,pl); - } else { + else + if (pl == 0) + return sprintf("%d", a); + else + return sprintf("%d%d", a, pl); + } + else { if (a >= 10 || pl >= 10) { - return sprintf("%02d%02d#4#%02d",a,pl,b); + return sprintf("%02d%02d#4#%02d", a, pl, b); } - return sprintf("%d%d#4#%02d",a,pl,b); + return sprintf("%d%d#4#%02d", a, pl, b); } } _addressToSlashes(_address) { - if (_address.length == 4) return sprintf("0/%d/%d", parseInt(_address.substring(0,2)),parseInt(_address.substring(2,4))) - if (_address.length == 2) return sprintf("0/%d/%d", parseInt(_address.substring(0,1)),parseInt(_address.substring(1,2))) + if (_address.length == 4) return sprintf("0/%d/%d", parseInt(_address.substring(0, 2)), parseInt(_address.substring(2, 4))); + if (_address.length == 2) return sprintf("0/%d/%d", parseInt(_address.substring(0, 1)), parseInt(_address.substring(1, 2))); + if (_address.length == 1) return sprintf("0/%d/0", parseInt(_address)); // TODO return "9/99/99"; } @@ -82,60 +90,82 @@ class MyHomeClient { * Avoid repeated calls of the same command on a short time, it can happen with the dimmer control on iOS * It buffer the commands and sends the last one. */ - _bufferCommand(_class,_address,_command, _timeout) { - if (this.buffers[sprintf("%s-%s",_class,_address)] != null) { - clearTimeout(this.buffers[sprintf("%s-%s",_class,_address)]); + _bufferCommand(_class, _address, _command, _timeout) { + if (this.buffers[sprintf("%s-%s", _class, _address)] != null) { + clearTimeout(this.buffers[sprintf("%s-%s", _class, _address)]); } - this.buffers[sprintf("%s-%s",_class,_address)] = setTimeout(function() { + this.buffers[sprintf("%s-%s", _class, _address)] = setTimeout(function () { this.command.send(_command); - delete(this.buffers[sprintf("%s-%s",_class,_address)]); + delete (this.buffers[sprintf("%s-%s", _class, _address)]); }.bind(this), _timeout); } - relayCommand(_address,_on) { + relayCommand(_address, _on) { var address = this._slashesToAddress(_address); if (address == "") return; - this.command.send(sprintf("*1*%d*%s##",(_on)?1:0,address)); + this.command.send(sprintf("*1*%d*%s##", (_on) ? 1 : 0, address)); } - simpleBlindCommand(_address,_stopUpDown) { + relayTimedOn(_address, hh, mm, ss) { + var address = this._slashesToAddress(_address); + if (address == "") return; + this.command.send(sprintf("*#1*%s*#2*%d*%d*%d##", address, hh, mm, ss)); + } + + simpleBlindCommand(_address, _stopUpDown) { var address = this._slashesToAddress(_address); if (address == "") return; /* Send a starting stop */ if (_stopUpDown != 0) { - this.command.send(sprintf("*2*%d*%s##",0,address)); + this.command.send(sprintf("*2*%d*%s##", 0, address)); } - this.command.send(sprintf("*2*%d*%s##",parseInt(_stopUpDown),address)); - } + //this.command.send(sprintf("*2*%d*%s##", parseInt(_stopUpDown), address)); + this._bufferCommand("BLIND", address, sprintf("*2*%d*%s##", parseInt(_stopUpDown), address), 500); + } - advancedBlindCommand(_address,_shutterLevel) { + advancedBlindCommand(_address, _shutterLevel) { var address = this._slashesToAddress(_address); if (address == "") return; - this.command.send(sprintf("*#2*%s*#11#1*%d##",address,parseInt(_shutterLevel))); - } + this.command.send(sprintf("*#2*%s*#11#1*%d##", address, parseInt(_shutterLevel))); + } + + dimmerCommand(_address, _bri) { + // 2013.04.12 - angeloxx - LG explains speed parameter of the *#1**#1** OWN frame + // Speed is the single-step-time in 10ms scale. So the ramp time is: + // time = * 10ms * , where delta is abs(currentlevel-finallevel). + // speed = ( / 10ms) / , delta level is [current|final]level*1000, ramptime is in ms - dimmerCommand(_address,_bri) { - // 2013.04.12 - angeloxx - LG explains speed parameter of the *#1**#1** OWN frame - // Speed is the single-step-time in 10ms scale. So the ramp time is: - // time = * 10ms * , where delta is abs(currentlevel-finallevel). - // speed = ( / 10ms) / , delta level is [current|final]level*1000, ramptime is in ms - var address = this._slashesToAddress(_address); if (address == "") return; if (_bri > 0) { - this._bufferCommand("DIMMER",address,sprintf("*#1*%s*#1*%s*1##",address,_bri+100),500); + this._bufferCommand("DIMMER", address, sprintf("*#1*%s*#1*%s*1##", address, _bri + 100), 500); } else { - this._bufferCommand("DIMMER",address,sprintf("*1*0*%s##",address),500); + this._bufferCommand("DIMMER", address, sprintf("*1*0*%s##", address), 500); } } - setSetPoint(_address,_temperature) { - // Standard thermostat *4*40*%02d##*#4*#%02d*#14*%04d*3 - this._bufferCommand("THERMO",_address,sprintf("*4*40*%02d##*#4*#%02d*#14*%04d*3##*#4*%02d*14##",_address,_address,_temperature * 10,_address),500); + forcedLoadCommand(_address, _forced) { + if (_address == "") return; + if (_forced == 1) + this.command.send(sprintf("*3*2*#%d##", _address)); + } + + enableScenarioCommand(_address, _status) { + if (_address == "") return; + this.command.send(sprintf("*17*%d*%s##", (_status) ? 3 : 4, _address)); + } + runScenarioCommand(_address, _status) { + if (_address == "") return; + this.command.send(sprintf("*17*%d*%s##", (_status) ? 1 : 2, _address)); + } + + setSetPoint(_address, _temperature) { + // Standard thermostat *4*40*%02d##*#4*#%02d*#14*%04d*3 + this._bufferCommand("THERMO", _address, sprintf("*4*40*%02d##*#4*#%02d*#14*%04d*3##*#4*%02d*14##", _address, _address, _temperature * 10, _address), 500); // 4 Zones *#4*#0#%02d*#14*%04d*3 } @@ -144,26 +174,32 @@ class MyHomeClient { * sends a light status request for all known buses */ onCommandConnect() { - this.lightBuses.forEach(function(element) { + this.lightBuses.forEach(function (element) { if (element == 0) { this.command.send("*#1*0##"); } else { - this.command.send(sprintf("*#1*0#4#%02d##",element)); + this.command.send(sprintf("*#1*0#4#%02d##", element)); } }, this); + //request also AUX states + this.command.send("*#9##"); + + //request also controlled load states + this.command.send("*#3##"); + /* Light status well keep some seconds, so I prefer to wait to send the connect feedback */ - setTimeout(function() { - if (this.parent != null && this.parent.onConnect != null && typeof(this.parent.onConnect) == 'function') this.parent.onConnect(); - }.bind(this),this.lightBuses.length * 2500); + setTimeout(function () { + if (this.parent != null && this.parent.onConnect != null && typeof (this.parent.onConnect) == 'function') this.parent.onConnect(); + }.bind(this), this.lightBuses.length * 2500); } onMonitor(_frame) { // Split frame (TODO: and join it if is a long one that is splitted in multiple input) var frames = _frame.split("##"); - frames.forEach(function(frame) { + frames.forEach(function (frame) { if (frame == "") return; - if (this.parent != null && this.parent.onMonitor != null && typeof(this.parent.onMonitor) == 'function') this.parent.onMonitor(frame); + if (this.parent != null && this.parent.onMonitor != null && typeof (this.parent.onMonitor) == 'function') this.parent.onMonitor(frame); /* Try to decode as... */ var extract; @@ -171,93 +207,103 @@ class MyHomeClient { /* Light level */ extract = frame.match(/^\*1\*(\d+)\*([0-9#]+)$/); if (extract) { - var address = this._addressToSlashes(extract[2]); + let address = this._addressToSlashes(extract[2]); if (extract[1] <= 1) { - if (this.parent != null && this.parent.onMonitor != null && typeof(this.parent.onRelay) == 'function') this.parent.onRelay(address,extract[1] == 1); - debug(sprintf("LIGHTLEVEL %s ADDRESS %s",extract[1],address)); - } else { - if (this.parent != null && this.parent.onMonitor != null && typeof(this.parent.onDimmer) == 'function') this.parent.onDimmer(address,this.dimmerLevels[parseInt(extract[1],10)]); - } + if (this.parent != null && this.parent.onMonitor != null && typeof (this.parent.onRelay) == 'function') this.parent.onRelay(address, extract[1] == 1); + debug(sprintf("LIGHTLEVEL %s ADDRESS %s", extract[1], address)); + } else + if (parseInt(extract[1], 10) <= 10) { + if (this.parent != null && this.parent.onMonitor != null && typeof (this.parent.onDimmer) == 'function') this.parent.onDimmer(address, this.dimmerLevels[parseInt(extract[1], 10)]); + } + } + + /* Relay time duration */ + // *#1*12*2*190*1*5##> + extract = frame.match(/^\*#1\*([0-9#]+)\*2\*(\d+)\*(\d+)\*(\d+)$/); + if (extract) { + let address = this._addressToSlashes(extract[1]); + if (this.parent != null && this.parent.onMonitor != null && typeof (this.parent.onRelayDuration) == 'function') + this.parent.onRelayDuration(address, parseInt(extract[2], 10) * 3600 + parseInt(extract[3], 10) * 60 + parseInt(extract[4], 10)); } /* Light level - Advanced (VantageControls) way */ // *#1*12*1*190*1##> extract = frame.match(/^\*#1\*([0-9#]+)\*\d\*(\d+)\*\d+$/); if (extract) { - var address = this._addressToSlashes(extract[1]); - if (this.parent != null && this.parent.onMonitor != null && typeof(this.parent.onDimmer) == 'function') this.parent.onDimmer(address,parseInt(extract[2],10)-100); + let address = this._addressToSlashes(extract[1]); + if (this.parent != null && this.parent.onMonitor != null && typeof (this.parent.onDimmer) == 'function') this.parent.onDimmer(address, parseInt(extract[2], 10) - 100); } /* Simple (F411) Blind */ - extract = frame.match(/^\*2\*(\d)\*([0-9#]+)$/); + extract = frame.match(/^\*2\*(\d)\*([0-9#]+)$/); if (extract) { - var address = this._addressToSlashes(extract[2]); - debug(sprintf("BLIND %s ADDRESS %s",extract[1],address)); - if (this.parent != null && this.parent.onMonitor != null && typeof(this.parent.onSimpleBlind) == 'function') this.parent.onSimpleBlind(address,parseInt(extract[1],10)); + let address = this._addressToSlashes(extract[2]); + debug(sprintf("BLIND %s ADDRESS %s", extract[1], address)); + if (this.parent != null && this.parent.onMonitor != null && typeof (this.parent.onSimpleBlind) == 'function') this.parent.onSimpleBlind(address, parseInt(extract[1], 10)); } /* Advanced (F401) Blind (on stop) */ - extract = frame.match(/^\*#2\*([0-9#]+)\*10\*(\d+)\*(\d+)\*\d+\*\d+$/); + extract = frame.match(/^\*#2\*([0-9#]+)\*10\*(\d+)\*(\d+)\*\d+\*\d+$/); if (extract) { - var address = this._addressToSlashes(extract[1]); - var position = parseInt(extract[3],10); - var direction = "STOP"; + let address = this._addressToSlashes(extract[1]); + let position = parseInt(extract[3], 10); + let direction = "STOP"; - if (parseInt(extract[2],10) == 11) direction = "UP"; - if (parseInt(extract[2],10) == 12) direction = "DOWN"; + if (parseInt(extract[2], 10) == 11) direction = "UP"; + if (parseInt(extract[2], 10) == 12) direction = "DOWN"; - debug(sprintf("ADVANCEDBLIND %s/%s ADDRESS %s",position,direction,address)); - if (this.parent != null && this.parent.onMonitor != null && typeof(this.parent.onSimpleBlind) == 'function') this.parent.onAdvancedBlind(address,direction,position); + debug(sprintf("ADVANCEDBLIND %s/%s ADDRESS %s", position, direction, address)); + if (this.parent != null && this.parent.onMonitor != null && typeof (this.parent.onSimpleBlind) == 'function') this.parent.onAdvancedBlind(address, direction, position); } /* Ambient temperature */ extract = frame.match(/^\*#4\*(\d+)\*[0|14]\*(\d\d\d\d)$/); if (extract) { - var address = parseInt(extract[1]); - var temperature = parseFloat(extract[2]/10); + let address = parseInt(extract[1]); + let temperature = parseFloat(extract[2] / 10); if (temperature > 100) { temperature = -(temperature - 100); } - if (this.parent != null && this.parent.onMonitor != null && typeof(this.parent.onThermostat) == 'function') this.parent.onThermostat(address,"AMBIENT",temperature); - debug(sprintf("AMBIENT %s ADDRESS %s",temperature,address)); + if (this.parent != null && this.parent.onMonitor != null && typeof (this.parent.onThermostat) == 'function') this.parent.onThermostat(address, "AMBIENT", temperature); + debug(sprintf("AMBIENT %s ADDRESS %s", temperature, address)); } /* Setpoint temperature */ extract = frame.match(/^\*#4\*(\d+)\*12\*(\d\d\d\d)\*(\d+)$/); if (extract) { - var address = parseInt(extract[1]); - var temperature = parseFloat(extract[2]/10); + let address = parseInt(extract[1]); + let temperature = parseFloat(extract[2] / 10); if (temperature > 100) { temperature = -(temperature - 100); } - if (this.parent != null && this.parent.onMonitor != null && typeof(this.parent.onThermostat) == 'function') this.parent.onThermostat(address,"SETPOINT",temperature); - debug(sprintf("SETPOINT %s ADDRESS %s",temperature,address)); + if (this.parent != null && this.parent.onMonitor != null && typeof (this.parent.onThermostat) == 'function') this.parent.onThermostat(address, "SETPOINT", temperature); + debug(sprintf("SETPOINT %s ADDRESS %s", temperature, address)); } // External sensor (see http://www.myopen-legrandgroup.com/community/italian_my_open/italian_65536/f/152/p/1476/7590.aspx#7590) // Format *#4*
00*15*1**0001## extract = frame.match(/^\*#4\*(\d+)00\*15\*1\*(\d\d\d\d)\*0001$/); if (extract) { - var address = parseInt(extract[1]); - var temperature = parseFloat(extract[2]/10); + let address = parseInt(extract[1]); + let temperature = parseFloat(extract[2] / 10); if (temperature > 100) { temperature = -(temperature - 100); } - if (this.parent != null && this.parent.onMonitor != null && typeof(this.parent.onThermometer) == 'function') this.parent.onThermometer(address,"AMBIENT",temperature); - debug(sprintf("EXT-AMBIENT %s ADDRESS %s",temperature,address)); - } + if (this.parent != null && this.parent.onMonitor != null && typeof (this.parent.onThermometer) == 'function') this.parent.onThermometer(address, "AMBIENT", temperature); + debug(sprintf("EXT-AMBIENT %s ADDRESS %s", temperature, address)); + } // new regex:thActuators = RegEx_Create( \"^\*#4\*(\d{1,2})\*19\*(\d)\*(\d)$" ); extract = frame.match(/^\*#4\*(\d+)\*19\*(\d)\*(\d)$/); if (extract) { - var address = parseInt(extract[1]); - var cooling = inArray(extract[2],[1,2]); - var heating = inArray(extract[3],[1,2]); - if (this.parent != null && this.parent.onMonitor != null && typeof(this.parent.onThermostat) == 'function') this.parent.onThermostat(address,"HEATING",heating); - if (this.parent != null && this.parent.onMonitor != null && typeof(this.parent.onThermostat) == 'function') this.parent.onThermostat(address,"COOLING",cooling); - debug(sprintf("HEATING %s COOLING %s ADDRESS %s",heating,cooling,address)); - } + let address = parseInt(extract[1]); + let cooling = inArray(extract[2], [1, 2]); + let heating = inArray(extract[3], [1, 2]); + if (this.parent != null && this.parent.onMonitor != null && typeof (this.parent.onThermostat) == 'function') this.parent.onThermostat(address, "HEATING", heating); + if (this.parent != null && this.parent.onMonitor != null && typeof (this.parent.onThermostat) == 'function') this.parent.onThermostat(address, "COOLING", cooling); + debug(sprintf("HEATING %s COOLING %s ADDRESS %s", heating, cooling, address)); + } // *#4*#*20*## extract = frame.match(/^\*#4\*(\d+)#\d\*20\*\d$/); @@ -270,24 +316,172 @@ class MyHomeClient { // *25*32#[0-1]*3## -> Open (0 after a state request, 1 after an event) -> FALSE extract = frame.match(/^\*25\*3(\d)#[0-1]\*3(\d+)$/); if (extract) { - var value = parseInt(extract[1],10) == 1 ? true : false; - var address = parseInt(extract[2],10); - if (this.parent != null && this.parent.onMonitor != null && typeof(this.parent.onContactSensor) == 'function') this.parent.onContactSensor(address,value); - debug(sprintf("DRYCONTACT %s ADDRESS %s",value,address)); - } + let value = parseInt(extract[1], 10) == 1 ? true : false; + let address = parseInt(extract[2], 10); + if (this.parent != null && this.parent.onMonitor != null && typeof (this.parent.onDryContact) == 'function') this.parent.onDryContact(address, value); + debug(sprintf("DRYCONTACT %s ADDRESS %s", value, address)); + } + + // AUX: *9*[0,9]*## OFF + // AUX: *9*1*## ON + extract = frame.match(/^\*9\*(\d)\*(\d)$/); + if (extract) { + let value = false; + if (parseInt(extract[1], 10) == 0 || parseInt(extract[1], 10) == 9) + value = false; + else + value = true; + let address = parseInt(extract[2], 10); + if (this.parent != null && this.parent.onMonitor != null && typeof (this.parent.onAUX) == 'function') + this.parent.onAUX(address, value); + debug(sprintf("AUX %s ADDRESS %s", value, address)); + } + + // Scenario: *17*1*## Start + // Scenario: *17*2*## Stop + // Scenario: *17*3*## Enable + // Scenario: *17*4*## Disable + extract = frame.match(/^\*17\*(\d)\*(\d+)$/); + if (extract) { + let value = false; + let command = parseInt(extract[1], 10); + if ((command == 3) || (command == 1)) + value = true; + let address = parseInt(extract[2], 10); + if (this.parent != null && this.parent.onMonitor != null && typeof (this.parent.onScenarioEnable) == 'function' && (command == 3 || command == 4)) + this.parent.onScenarioEnable(address, value); + if (this.parent != null && this.parent.onMonitor != null && typeof (this.parent.onScenarioRun) == 'function' && (command == 1 || command == 2)) + this.parent.onScenarioRun(address, value); + debug(sprintf("Scenario %s ADDRESS %s", value, address)); + } + + //powermeter + extract = frame.match(/^\*#3\*10\*3\*(\d+)$/); + if (extract) { + let value = parseInt(extract[1], 10); + if (this.parent != null && this.parent.onMonitor != null && typeof (this.parent.onPowerMeter) == 'function') + this.parent.onPowerMeter(value); + debug(sprintf("Power %s W", value)); + } + + // + //controlled load + extract = frame.match(/^\*3\*(\d)\*#(\d)$/); + if (extract) { + let value = parseInt(extract[1], 10); + let address = parseInt(extract[2], 10); + if (this.parent != null && this.parent.onMonitor != null && typeof (this.parent.onControlledLoad) == 'function') + this.parent.onControlledLoad(address, value); + } + + //alarm status + extract = frame.match(/^\*5\*(\d+)\*(0*)$/); + if (extract) { + let value = parseInt(extract[1], 10); + switch (value) { + case 0: //maintenance + if (this.parent != null && this.parent.onMonitor != null && typeof (this.parent.onAlarmFault) == 'function') + this.parent.onAlarmFault(1); + break; + case 1: //active + if (this.parent != null && this.parent.onMonitor != null && typeof (this.parent.onAlarmFault) == 'function') + this.parent.onAlarmFault(0); + break; + case 8: //engaged + if (this.parent != null && this.parent.onMonitor != null && typeof (this.parent.onAlarm) == 'function') + this.parent.onAlarm(1); + break; + case 9: //disengaged + if (this.parent != null && this.parent.onMonitor != null && typeof (this.parent.onAlarm) == 'function') + this.parent.onAlarm(3); + if (this.parent != null && this.parent.onMonitor != null && typeof (this.parent.onAlarmTampered) == 'function') + this.parent.onAlarmTampered(0); + break; + case 4: + case 10: //battery KO + if (this.parent != null && this.parent.onMonitor != null && typeof (this.parent.onAlarmLowBattery) == 'function') + this.parent.onAlarmLowBattery(1); + break; + case 5: //battery OK + if (this.parent != null && this.parent.onMonitor != null && typeof (this.parent.onAlarmLowBattery) == 'function') + this.parent.onAlarmLowBattery(0); + break; + case 6: //no network + if (this.parent != null && this.parent.onMonitor != null && typeof (this.parent.onAlarmNetwork) == 'function') + this.parent.onAlarmNetwork(0); + break; + case 7: //network ok + if (this.parent != null && this.parent.onMonitor != null && typeof (this.parent.onAlarmNetwork) == 'function') + this.parent.onAlarmNetwork(1); + break; + default: + } + } + + + //alarm triggered and zone selection + extract = frame.match(/^\*5\*(\d+)\*#(\d)$/); + if (extract) { + let value = parseInt(extract[1], 10); + switch (value) { + case 11: //zone active + if (this.parent != null && this.parent.onMonitor != null && typeof (this.parent.onZoneActive) == 'function') + this.parent.onZoneActive(parseInt(extract[2], 10) - 1, true); + break; + case 18: //zone deactive + if (this.parent != null && this.parent.onMonitor != null && typeof (this.parent.onZoneActive) == 'function') + this.parent.onZoneActive(parseInt(extract[2], 10) - 1, false); + break; + case 15: //silent alarm + case 17: //intrusion + if (this.parent != null && this.parent.onMonitor != null && typeof (this.parent.onAlarm) == 'function') + this.parent.onAlarm(4); + break; + case 16: //tampering + if (this.parent != null && this.parent.onMonitor != null && typeof (this.parent.onAlarmTampered) == 'function') + this.parent.onAlarmTampered(1); + break; + default: + } + } }.bind(this)); } getThermostatStatus(_address) { - this.command.send(sprintf("*#4*%d##*#4*19*%d##",_address,_address)); + this.command.send(sprintf("*#4*%d##*#4*19*%d##", _address, _address)); } getContactState(_address) { - this.command.send(sprintf("*#25*3%d##",_address)); + this.command.send(sprintf("*#25*3%d##", _address)); + } + + getScenarioState(_address) { + this.command.send(sprintf("*#17*%d##", _address)); + } + + getRelayState(_address) { + this.command.send(sprintf("*#1*%s##", this._slashesToAddress(_address))); + } + + getRelayDuration(_address) { + this.command.send(sprintf("*#1*%s*2##", this._slashesToAddress(_address))); + } + + getAdvancedBlindState(_address) { + this.command.send(sprintf("*#2*%s*10##", this._slashesToAddress(_address))); } - getAdvancedBlindSate(_address) { - this.command.send(sprintf("*#2*%s*10##",this._slashesToAddress(_address))); + getPower() { + this.command.send("*#3*10*3##"); + } + + getAlarmState() { + this.command.send("*#5*0##"); + + } + + getControlledLoadState() { + this.command.send("*#3##"); } onCommand(_frame) { @@ -300,11 +494,11 @@ class MyHomeClient { */ class MyHomeConnection { - constructor(_ipaddress, _port, _password, _type, _onconnectcallback, _onmessagecallback) { + constructor(_ipaddress, _port, _password, _type, _onconnectcallback, _onmessagecallback, _setclock) { this.ipaddress = _ipaddress; this.password = _password; this.type = _type; - this.port = parseInt(_port,10); + this.port = parseInt(_port, 10); this.state = 0; this.expected = ""; this.isConnected = false; @@ -316,29 +510,33 @@ class MyHomeConnection { this.timerhandle = null; this.reconnecthandle = null; this.disconnecthandle = null; - + this.setclock = _setclock; + this.keepaliveSeconds = 25; /* Send keepalive command frame every 25 seconds */ this.idleSeconds = 55; /* Drop idle/lost connections after 55 seconds */ this.reconnectSeconds = 5; /* reconnect after 5 seconds */ + this.updateTimeSeconds = 3600; /* Keepalive frame on command connection every this.keepaliveSeconds seconds */ if (this.type == "COMMAND") { - setInterval(function(){ this.sendKeepalive(); }.bind(this), this.keepaliveSeconds * 1000); + setInterval(function () { this.sendKeepalive(); }.bind(this), this.keepaliveSeconds * 1000); + if (this.setclock) + setInterval(function () { this.updateDate(); }.bind(this), this.updateTimeSeconds * 1000); } this.connect(); } connect() { - this.connection = net.connect({ host: this.ipaddress, port: this.port }, () => {}); + this.connection = net.connect({ host: this.ipaddress, port: this.port }, () => { }); if (this.reconnecthandle != null) { clearTimeout(this.reconnecthandle); this.reconnecthandle = null; } /* Disconnect after this.idleSeconds seconds of inactivity */ if (this.disconnecthandle != null) clearTimeout(this.disconnecthandle); - this.disconnecthandle = setTimeout(function() { + this.disconnecthandle = setTimeout(function () { debug('Info[%s]: Stale connection detected, drop it!', this.type); - this.connection.end(); + this.connection.end(); }.bind(this), this.idleSeconds * 1000); this.connection.on('data', (_in) => { @@ -346,13 +544,13 @@ class MyHomeConnection { /* Disconnect after this.idleSeconds seconds of inactivity */ if (this.disconnecthandle != null) clearTimeout(this.disconnecthandle); - this.disconnecthandle = setTimeout(function() { + this.disconnecthandle = setTimeout(function () { debug('Info[%s]: Stale connection detected, drop it!', this.type); - this.connection.end(); + this.connection.end(); }.bind(this), this.idleSeconds * 1000); var data = _in.toString().trim(); - debug('Received[%s]: <%s>', this.type, data) + debug('Received[%s]: <%s>', this.type, data); if (this.state == 0 && data == "*#*1##") { /* * Great, answers and is ready to talk with me, I send @@ -370,7 +568,7 @@ class MyHomeConnection { debug('Info[%s]: Unauthenticated connection', this.type); this.state = 100; - this.buffer.forEach(function(item, index, object) { + this.buffer.forEach(function (item, index, object) { this.write(this.buffer[index]); this.buffer.splice(index, 1); }.bind(this)); @@ -380,7 +578,7 @@ class MyHomeConnection { /* OPEN auth, there's a rosetta implementation that "can be used" */ debug('Info[%s]: OWN Password Supported, try to answer', this.type); this.state = 21; - this.write(sprintf("*#%s##",this.openwebnetAnswer(this.password,data.match(/^\*#(\d{8,12})##$/)[1]))); + this.write(sprintf("*#%s##", this.openwebnetAnswer(this.password, data.match(/^\*#(\d{8,12})##$/)[1]))); } else if (this.state == 1 && data == "*98*2##") { /* HMAC is supported, ok let's try */ this.state = 10; @@ -389,7 +587,7 @@ class MyHomeConnection { } else if (this.state == 10) { /* RA offered, we can calc answer */ this.state = 11; - var ra = data.substring(2,data.length-2); + var ra = data.substring(2, data.length - 2); var hmac = this.calcHMAC(ra); this.write(hmac); debug('Info[%s]: HMAC Supported, proceed with phase 2', this.type); @@ -398,7 +596,7 @@ class MyHomeConnection { /* Yeah, connection is up! */ this.state = 100; this.write("*#*1##"); - this.buffer.forEach(function(item, index, object) { + this.buffer.forEach(function (item, index, object) { this.write(this.buffer[index]); this.buffer.splice(index, 1); }.bind(this)); @@ -415,7 +613,7 @@ class MyHomeConnection { if (data == "*#*1##") { debug('Info[%s]: OWN Password authenticated with success', this.type); this.state = 100; - this.buffer.forEach(function(item, index, object) { + this.buffer.forEach(function (item, index, object) { this.write(this.buffer[index]); this.buffer.splice(index, 1); }.bind(this)); @@ -433,28 +631,28 @@ class MyHomeConnection { this.state = 0; this.isConnected = false; if (this.reconnecthandle != null) clearTimeout(this.reconnecthandle); - this.reconnecthandle = setTimeout(function() { + this.reconnecthandle = setTimeout(function () { debug('Info[%s]: Try to reconnect !', this.type); this.connect(); - }.bind(this), this.reconnectSeconds * 1000) + }.bind(this), this.reconnectSeconds * 1000); }); this.connection.on('error', (_error) => { this.state = 0; this.isConnected = false; if (this.reconnecthandle != null) clearTimeout(this.reconnecthandle); - this.reconnecthandle = setTimeout(function() { + this.reconnecthandle = setTimeout(function () { debug('Info[%s]: Try to reconnect !', this.type); this.connect(); - }.bind(this), this.reconnectSeconds * 1000) - }); + }.bind(this), this.reconnectSeconds * 1000); + }); } /** * OWN Password implementation that can be found on the web, it seems to work */ - openwebnetAnswer(pass,nonce) { - var _0x9148=["\x30","\x31","\x32","\x33","\x34","\x35","\x36","\x37","\x38","\x39"];var _0xba8b=[_0x9148[0],_0x9148[1],_0x9148[2],_0x9148[3],_0x9148[4],_0x9148[5],_0x9148[6],_0x9148[7],_0x9148[8],_0x9148[9]];var flag=true;var num1=0x0;var num2=0x0;var password=parseInt(pass,10);for(var c in nonce){c= nonce[c];if(c!= _0xba8b[0]){if(flag){num2= password};flag= false};switch(c){case _0xba8b[1]:num1= num2& 0xFFFFFF80;num1= num1>>> 7;num2= num2<< 25;num1= num1+ num2;break;case _0xba8b[2]:num1= num2& 0xFFFFFFF0;num1= num1>>> 4;num2= num2<< 28;num1= num1+ num2;break;case _0xba8b[3]:num1= num2& 0xFFFFFFF8;num1= num1>>> 3;num2= num2<< 29;num1= num1+ num2;break;case _0xba8b[4]:num1= num2<< 1;num2= num2>>> 31;num1= num1+ num2;break;case _0xba8b[5]:num1= num2<< 5;num2= num2>>> 27;num1= num1+ num2;break;case _0xba8b[6]:num1= num2<< 12;num2= num2>>> 20;num1= num1+ num2;break;case _0xba8b[7]:num1= num2& 0x0000FF00;num1= num1+ ((num2& 0x000000FF)<< 24);num1= num1+ ((num2& 0x00FF0000)>>> 16);num2= (num2& 0xFF000000)>>> 8;num1= num1+ num2;break;case _0xba8b[8]:num1= num2& 0x0000FFFF;num1= num1<< 16;num1= num1+ (num2>>> 24);num2= num2& 0x00FF0000;num2= num2>>> 8;num1= num1+ num2;break;case _0xba8b[9]:num1= ~num2;break;case _0xba8b[0]:num1= num2;break};num2= num1};return (num1>>> 0).toString() + openwebnetAnswer(pass, nonce) { + var _0x9148 = ["\x30", "\x31", "\x32", "\x33", "\x34", "\x35", "\x36", "\x37", "\x38", "\x39"]; var _0xba8b = [_0x9148[0], _0x9148[1], _0x9148[2], _0x9148[3], _0x9148[4], _0x9148[5], _0x9148[6], _0x9148[7], _0x9148[8], _0x9148[9]]; var flag = true; var num1 = 0x0; var num2 = 0x0; var password = parseInt(pass, 10); for (var c in nonce) { c = nonce[c]; if (c != _0xba8b[0]) { if (flag) { num2 = password; } flag = false; } switch (c) { case _0xba8b[1]: num1 = num2 & 0xFFFFFF80; num1 = num1 >>> 7; num2 = num2 << 25; num1 = num1 + num2; break; case _0xba8b[2]: num1 = num2 & 0xFFFFFFF0; num1 = num1 >>> 4; num2 = num2 << 28; num1 = num1 + num2; break; case _0xba8b[3]: num1 = num2 & 0xFFFFFFF8; num1 = num1 >>> 3; num2 = num2 << 29; num1 = num1 + num2; break; case _0xba8b[4]: num1 = num2 << 1; num2 = num2 >>> 31; num1 = num1 + num2; break; case _0xba8b[5]: num1 = num2 << 5; num2 = num2 >>> 27; num1 = num1 + num2; break; case _0xba8b[6]: num1 = num2 << 12; num2 = num2 >>> 20; num1 = num1 + num2; break; case _0xba8b[7]: num1 = num2 & 0x0000FF00; num1 = num1 + ((num2 & 0x000000FF) << 24); num1 = num1 + ((num2 & 0x00FF0000) >>> 16); num2 = (num2 & 0xFF000000) >>> 8; num1 = num1 + num2; break; case _0xba8b[8]: num1 = num2 & 0x0000FFFF; num1 = num1 << 16; num1 = num1 + (num2 >>> 24); num2 = num2 & 0x00FF0000; num2 = num2 >>> 8; num1 = num1 + num2; break; case _0xba8b[9]: num1 = ~num2; break; case _0xba8b[0]: num1 = num2; break; }num2 = num1; } return (num1 >>> 0).toString(); } /** @@ -473,9 +671,9 @@ class MyHomeConnection { if (this.buffer.length > 0) { this.write(this.buffer.shift()); if (this.buffer.length > 0) { - this.timerhandle = setTimeout(function() { + this.timerhandle = setTimeout(function () { this.writeBuffer(); - }.bind(this), this.commandms); + }.bind(this), this.commandms); } else { this.timerhandle = null; } @@ -486,7 +684,7 @@ class MyHomeConnection { send(_in) { var frames = _in.split("##"); - frames.forEach(function(frame) { + frames.forEach(function (frame) { if (frame == "") return; frame = frame + "##"; if (this.isConnected) { @@ -497,18 +695,18 @@ class MyHomeConnection { debug('Buffer[%s]: <%s>', this.type, frame); this.buffer.push(frame); if (this.timerhandle == null) { - this.timerhandle = setTimeout(function() { + this.timerhandle = setTimeout(function () { this.writeBuffer(); - }.bind(this), this.commandms); + }.bind(this), this.commandms); } } } else { debug('Buffer[%s]: <%s>', this.type, frame); this.buffer.push(frame); if (this.timerhandle == null) { - this.timerhandle = setTimeout(function() { + this.timerhandle = setTimeout(function () { this.writeBuffer(); - }.bind(this), this.commandms); + }.bind(this), this.commandms); } } }, this); @@ -516,7 +714,17 @@ class MyHomeConnection { sendKeepalive() { debug('Keealive[%s]: send keepalive', this.type); - if (this.isConnected) this.connection.write("*#13**15##"); + if (this.isConnected) { + this.connection.write("*#13**15##"); + } + } + + updateDate() { + if (this.isConnected) { + var setDateTimeFrame = moment().format("*#13**#22*HH*mm*ss*001*0d*DD*MM*YYYY##"); + this.connection.write(setDateTimeFrame); + + } } calcHMAC(_in) { @@ -524,16 +732,16 @@ class MyHomeConnection { /* TODO: SHA1 */ } else if (_in.length == 128) { var rb = crypto.createHmac('sha256', "pimperepettenusa").digest('hex'); - var message = sha256(this.digitToHex(_in)+rb+"736F70653E"+"636F70653E"+sha256(this.password)); - this.answer = sprintf("*#%s##", this.hexToDigit(sha256(this.digitToHex(_in)+rb+sha256(this.password)))).trim(); - return sprintf("*#%s*%s##", this.hexToDigit(rb),this.hexToDigit(message)); + var message = sha256(this.digitToHex(_in) + rb + "736F70653E" + "636F70653E" + sha256(this.password)); + this.answer = sprintf("*#%s##", this.hexToDigit(sha256(this.digitToHex(_in) + rb + sha256(this.password)))).trim(); + return sprintf("*#%s*%s##", this.hexToDigit(rb), this.hexToDigit(message)); } } digitToHex(_in) { var out = ""; - for (var i = 0; i < _in.length; i = i+4) { - out = out + (parseInt(_in[i])*10+parseInt(_in[i+1])).toString(16) + (parseInt(_in[i+2])*10+parseInt(_in[i+3])).toString(16); + for (var i = 0; i < _in.length; i = i + 4) { + out = out + (parseInt(_in[i]) * 10 + parseInt(_in[i + 1])).toString(16) + (parseInt(_in[i + 2]) * 10 + parseInt(_in[i + 3])).toString(16); } return out; } @@ -541,7 +749,7 @@ class MyHomeConnection { hexToDigit(_in) { var out = ""; for (var i = 0; i < _in.length; i++) { - out = out + sprintf("%d%d", parseInt(_in[i],16)/10,parseInt(_in[i],16)%10); + out = out + sprintf("%d%d", parseInt(_in[i], 16) / 10, parseInt(_in[i], 16) % 10); } return out; } diff --git a/package.json b/package.json index dce4a10..6a9bffc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "homebridge-myhome-tng", - "version": "0.0.18", + "name": "homebridge-myhome", + "version": "0.1.19", "description": "Legrand MyHome plugin for homebridge: https://github.com/nfarina/homebridge", "license": "MIT", "keywords": [ @@ -11,22 +11,19 @@ "homebridge": ">=0.2.0" }, "author": { - "name": "Angelo Conforti" + "name": "Simone Tisa" }, "repository": { "type": "git", - "url": "https://github.com/angeloxx/homebridge-myhome" + "url": "https://github.com/simont77/homebridge-myhome" }, "dependencies": { - "colorsys": "^1.0.9", "debug": "^2.4.5", "net": "^1.0.0", - "promise": "^7.0.0", - "sha1": "^1.1.1", "sha256": "^0.2.0", - "sprint-js": "^0.1.0", "sprintf-js": "^1.0.3", - "wait-until": "^0.0.2", - "inherits": "^2.0.0" + "moment": "^2.18.1", + "correcting-interval": "^2.0.0", + "fakegato-history": "^0.6.3" } } diff --git a/sample-config.json b/sample-config.json deleted file mode 100644 index fcb3673..0000000 --- a/sample-config.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "platforms": [{ - "platform": "LegrandMyHome", - "ipaddress": "192.168.157.207", - "port": 20000, - "ownpassword": "12345", - "discovery": false, - "devices": [{ - "accessory": "MHRelay", - "name": "Bathroom Light", - "address": "0/1/5" - },{ - "accessory": "MHRelay", - "name": "Night hallway Light", - "address": "0/1/1" - },{ - "accessory": "MHRelay", - "name": "Office", - "address": "0/1/4" - },{ - "accessory": "MHDimmer", - "name": "Master bedroom Central", - "address": "0/1/2" - },{ - "accessory": "MHOutlet", - "name": "Master bedroom Thermal Blanket", - "address": "0/2/3" - },{ - "accessory": "MHContactSensor", - "name": "Bedroom presence sensor", - "address": "21" - },{ - "accessory": "MHThermostat", - "name": "Living Room Thermostat", - "address": "21" - },{ - "accessory": "MHExternalThermometer", - "name": "External Thermo Sensor", - "address": "5" - },{ - "accessory": "MHBlind", - "name": "Sample Curtain", - "address": "0/6/3", - "time": 10 - },{ - "accessory": "MHBlindAdvanced", - "name": "Sample Advanced Curtain", - "address": "0/6/4" - },{ - "accessory": "MHPowerMeter", - "name": "Sample Meter", - "address": "0/6/3", - "time": 10 - }] - }], - "bridge": { - "username": "CC:22:3D:E3:CE:31", - "name": "MyHome HomeBridge Adapter", - "pin": "023-42-522", - "port": 51827 - }, - "description": "My MyHome Home System", - "accessories": [ - ] -} \ No newline at end of file diff --git a/sample_config.json b/sample_config.json new file mode 100755 index 0000000..69e6881 --- /dev/null +++ b/sample_config.json @@ -0,0 +1,74 @@ +{ + "platforms": [{ + "platform": "LegrandMyHome", + "ipaddress": "192.168.1.2", + "port": 20000, + "ownpassword": "12345", + "setclock": true, + "devices": [{ + "accessory": "MHRelay", + "name": "Sample light", + "address": "0/1/2" + },{ + "accessory": "MHDimmer", + "name": "Sample dimmer", + "address": "0/1/1" + },{ + "accessory": "MHBlind", + "name": "Sample blind", + "address": "0/3/1", + "time": 28 + },{ + "accessory": "MHBlindAdvanced", + "name": "Sample Advanced Curtain", + "address": "0/3/2" + },{ + "accessory": "MHOutlet", + "name": "Sample outlet", + "address": "0/2/1", + "pul":true + },{ + "accessory": "MHTimedRelay", + "name": "Sample timed relay", + "address": "0/4/1", + "pul":true, + "timer":300 + },{ + "accessory": "MHPowerMeter", + "name": "Sample powermeter", + "refresh": 10 + },{ + "accessory": "MHAlarm", + "name": "Sample alarm" + },{ + "accessory": "MHDryContact", + "name": "Sample contact", + "address": "10", + "type": "Contact" + },{ + "accessory": "MHAux", + "name": "Sample aux", + "address": "3", + "type": "Leak" + },{ + "accessory": "MHControlledLoad", + "name": "Sample controlled load", + "address": "4" + },{ + "accessory": "MHThermostat", + "name": "Sample Thermostat", + "address": "21" + },{ + "accessory": "MHExternalThermometer", + "name": "Sample Thermo Sensor", + "address": "5" + }] + }], + "bridge": { + "username": "11:22:33:44:55:66", + "name": "MyHome HomeBridge Adapter", + "pin": "123-45-678", + "port": 51827 + }, + "description": "My MyHome Home System" +}