From 5d4f7674fa5a5bccbedaf1106631ff2247457b8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc?= <1536036+zoic21@users.noreply.github.com> Date: Sun, 19 Jan 2025 23:55:43 +0100 Subject: [PATCH] Support VR ipx800V4 + poll interval + url force refresh (#1) * begin get vr pos * add poll interval * add webhook handler * add install script * bugfix * fix * test * test * same * test * add more log * fix * add log * add log * add log * test * test * test * add log * typoi * test * fix * remove log * try * typo * remove log * add log * fix log * test * test * bugfix * test * test real time update position * typo * bugfix * test * same * test * add log * test * add log * bugfix * bugfix * bugfix * bugfix * test * test * test * test * test * test * improvement * improvement * update readme * improve chache restore * add more log * test * test * bugfix * test * add more log * test * test * add log * test * test * remove log * test * test * revert * remove const * add more config * test * test * fix * typo * add log * change log level * remove log * improve code * add name in place of model * Update gradual.ts * Update package.json * Update install.sh * Update install.sh * Update install.sh * Update install.sh * bugfix * bugfix * test * test * test * remove get (not usefull) * test * test verify result from ipx * bugfix * same * bugfix * test * test simulate error * typo debug log * bugfix * remove simulation of error * Update ipxV4.ts * add more log * Update ipxV4.ts * Update ipxV4.ts * typo * add more log * Update ipxV4.ts * test * test * bugfix * test * test * test * fix * typo * fix * test * test * test * add more log * add more delay * test * test * test * test * bugfix * test * same * test * test * test * test * typo * improvements * test * add log * reduce ipx call * test * test * test * test * fix * test * test * test * fix * typo * test * improve code * update readme * test * add verfiy on vr * test * bugfix * improve timming --- .vscode/settings.json | 2 +- README.md | 47 ++++--- config.schema.json | 11 ++ install.sh | 8 ++ package.json | 2 +- src/conf-definition/api.d.ts | 4 +- src/conf-definition/relays.d.ts | 3 +- src/config.d.ts | 6 + src/device/analogInput.ts | 14 +-- src/device/gradual.ts | 30 +++-- src/device/input.ts | 8 +- src/device/relay.ts | 18 ++- src/ipx/api.ts | 3 +- src/ipx/ipxV4.ts | 209 +++++++++++++++++++++++++------- src/ipx/ipxV5.ts | 49 ++++---- src/platform.ts | 90 ++++++-------- src/webhookServer.ts | 35 ++++++ 17 files changed, 364 insertions(+), 175 deletions(-) create mode 100644 install.sh create mode 100644 src/webhookServer.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 31b20d18..89d2e29c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,7 @@ { "files.eol": "\n", "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" }, "editor.rulers": [ 140 ], "eslint.enable": true diff --git a/README.md b/README.md index 5fe5ea17..23ce9959 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,3 @@ - -

- - - - -

- - # Homebridge IPX800 Plugin This plugin brings support of IPX800 to homekit. @@ -18,14 +9,12 @@ As now it support different devices : * analog inputs (ipx, x-thl) 2. ipx v4 * relays - * gradual (x-dimmer, x4Vr) without state update + * gradual (x-dimmer, x4Vr) * analog inputs (ipx, x-thl) This is heavily based on the hombridge plateform template it may let you control your ipx800 relays. - - ## Install Development Dependencies Using a terminal on the computer running homebridge: @@ -34,39 +23,38 @@ Using a terminal on the computer running homebridge: #clone plugin git clone https://github.com/Adrien-B/ipx800.git - # install dependency cd ipx800 -npm install -sudo npm install -g typescript rimraf - -# build and link plugin -npm run build -npm link #or sudo npm link +Chmod +x install.sh;./install.sh - -#(re)start homebridge if not done already homebridge -D ``` - ## Configure the plugin In homebridge set the ipx api settings * ip * api-key * version +* pollInterval +* webhookPort +* webhookPath See the following json snippet exemple:  ``` "api": { - "ip": "*.*.*.*", - "key": "*", - "version": "v5" - }, + "ip": "*.*.*.*", + "key": "*", + "version": "v5", + "pollInterval": 60, + "webhookPort": 58698, + "webhookPath": "TODO" + }, ``` +After this configuration you can trigger a refresh of state by calling https://IP_HOMEBRIDGE:webhookPort/webhookPath, you can for exemple add this in ipx800v4 push to trigger an update when relay state change + ### Configure v5 devices Than add all your devices (relays, dimmer, inputs). See the following json snippet exemple for v5:  @@ -128,6 +116,13 @@ See the following json snippet exemple for v5:  "index": "r3" } ], + "graduals": [ + { + "displayName": "corridor", + "type": "covering", + "anaIndex": "VR02" + } + ], "analogInputs": [ { "displayName": "séjour", diff --git a/config.schema.json b/config.schema.json index bed0d5ac..09330cb5 100644 --- a/config.schema.json +++ b/config.schema.json @@ -18,6 +18,17 @@ "default": "v4", "enum": ["v4", "v5"], "required": true + }, + "pollInterval": { + "type": "number", + "default": 60 + }, + "webhookPort": { + "type": "number" + }, + "webhookPath": { + "type": "string", + "default": "" } } }, diff --git a/install.sh b/install.sh new file mode 100644 index 00000000..2bc869e4 --- /dev/null +++ b/install.sh @@ -0,0 +1,8 @@ +git pull +npm install +npm run build +npm link +npm pack +mv homebridge-ipx800-1.0.0.tgz /homebridge +cd /homebridge +npm install homebridge-ipx800-1.0.0.tgz diff --git a/package.json b/package.json index 5b76d5b5..3f4fbd7d 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "license": "Apache-2.0", "repository": { "type": "git", - "url": "https://github.com/Adrien-B/ipx800" + "url": "https://github.com/zoic21/homebridge-ipx800.git" }, "bugs": { "url": "https://github.com/Adrien-B/ipx800/issues" diff --git a/src/conf-definition/api.d.ts b/src/conf-definition/api.d.ts index d1955d5b..daa95f19 100644 --- a/src/conf-definition/api.d.ts +++ b/src/conf-definition/api.d.ts @@ -9,5 +9,7 @@ export interface Api { ip: string; key: string; version: "v4" | "v5"; - + pollInterval: number; + webhookPort: number; + webhookPath: string; } diff --git a/src/conf-definition/relays.d.ts b/src/conf-definition/relays.d.ts index ac28ac0d..c476fa3b 100644 --- a/src/conf-definition/relays.d.ts +++ b/src/conf-definition/relays.d.ts @@ -10,10 +10,11 @@ export type OutletFrPrise = "outlet"; export type FanFrVentilation = "fan"; export type SwitchFrInterrupteur = "switch"; export type ButtonFrPoussoir = "bswitch"; +export type ToggleFrPoussoir = "toggle"; export type Valve = "valve"; export type Relays = { displayName: string; - type: (LightFrLumiere | OutletFrPrise | FanFrVentilation | SwitchFrInterrupteur | ButtonFrPoussoir | Valve) & string; + type: (LightFrLumiere | OutletFrPrise | FanFrVentilation | SwitchFrInterrupteur | ButtonFrPoussoir | Valve | ToggleFrPoussoir) & string; index: string; }; diff --git a/src/config.d.ts b/src/config.d.ts index a7b4dcba..8cd5869a 100644 --- a/src/config.d.ts +++ b/src/config.d.ts @@ -7,6 +7,9 @@ export type IpxIp = string; export type IpxApiKey = string; +export type IpxPollInterval = number; +export type IpxWebhookPort = number; +export type IpxWebhookPath = string; export type IpxVersion = IpxVersion1 & IpxVersion2; export type IpxVersion1 = V4 | V5; export type V4 = "v4"; @@ -17,5 +20,8 @@ export interface IpxApiConfiguration { ip?: IpxIp; key?: IpxApiKey; version?: IpxVersion; + pollInterval?: IpxPollInterval; + webhookPort?: IpxWebhookPort; + webhookPath?: IpxWebhookPath; [k: string]: unknown; } diff --git a/src/device/analogInput.ts b/src/device/analogInput.ts index 1bf696be..e05dd4ca 100644 --- a/src/device/analogInput.ts +++ b/src/device/analogInput.ts @@ -15,7 +15,7 @@ export class AnalogInputHandler { // set accessory information this.accessory.getService(this.platform.Service.AccessoryInformation)! .setCharacteristic(this.platform.Characteristic.Manufacturer, 'GCE-Electronic') - .setCharacteristic(this.platform.Characteristic.Model, this.platform.model); + .setCharacteristic(this.platform.Characteristic.Model, accessory.context.device.displayName); switch(accessory.context.device.type) { case 'humidity': { @@ -39,12 +39,12 @@ export class AnalogInputHandler { this.service.setCharacteristic(this.platform.Characteristic.Name, accessory.context.device.displayName); } - async updateAnaValue(state: number){ + async updateAnaValue(value: number){ if (this.characteristic === this.platform.Characteristic.CurrentAmbientLightLevel) { - state = Math.max(state, 0.1); - }// else if (this.index == "THL1-TEMP") { - // state = (state - 1); - // } - this.service.updateCharacteristic(this.characteristic, state); + value = Math.max(value, 0.1); + } + if(this.service.getCharacteristic(this.characteristic).value != value){ + this.service.updateCharacteristic(this.characteristic, value); + } } } diff --git a/src/device/gradual.ts b/src/device/gradual.ts index 26ad2171..74ffc0e7 100644 --- a/src/device/gradual.ts +++ b/src/device/gradual.ts @@ -9,6 +9,7 @@ export class GradualHandler { public readonly anaIndex: string = this.accessory.context.device.anaIndex; private service: Service; private readonly characteristic; + private state; constructor( private readonly platform: IPXPlatform, @@ -18,15 +19,13 @@ export class GradualHandler { // set accessory information this.accessory.getService(this.platform.Service.AccessoryInformation)! .setCharacteristic(this.platform.Characteristic.Manufacturer, 'GCE-Electronic') - .setCharacteristic(this.platform.Characteristic.Model, this.platform.model); + .setCharacteristic(this.platform.Characteristic.Model, accessory.context.device.displayName); switch(accessory.context.device.type) { case 'covering': { - this.service = this.accessory.getService(this.platform.Service.WindowCovering) || - this.accessory.addService(this.platform.Service.WindowCovering); + this.service = this.accessory.getService(this.platform.Service.WindowCovering) || this.accessory.addService(this.platform.Service.WindowCovering); this.service.setCharacteristic(this.platform.Characteristic.Name, accessory.context.device.displayName); - this.service.getCharacteristic(this.platform.Characteristic.TargetPosition) - .onSet((v) => ipx.setVRPosition(v, this.platform, this.accessory)); + this.service.getCharacteristic(this.platform.Characteristic.TargetPosition).onSet((v) => ipx.setVRPosition(v, this.platform, this.accessory)); this.characteristic = this.platform.Characteristic.CurrentPosition; //but onSet is TargetPosition break; } @@ -51,14 +50,27 @@ export class GradualHandler { async updateAnaValue(value: number){ if (this.characteristic === this.platform.Characteristic.CurrentPosition) { - //if curtain (xv4r) revert position - this.service.updateCharacteristic(this.characteristic, 100 - value); + this.state = 100 - value; + if(this.service.getCharacteristic(this.characteristic).value != (100 - value)){ + this.service.updateCharacteristic(this.characteristic, 100 - value); + this.service.updateCharacteristic(this.platform.Characteristic.PositionState, this.platform.Characteristic.PositionState.STOPPED); + this.service.updateCharacteristic(this.platform.Characteristic.TargetPosition, 100 - value); + } + if(this.service.getCharacteristic(this.platform.Characteristic.PositionState).value != this.platform.Characteristic.PositionState.STOPPED){ + this.service.updateCharacteristic(this.platform.Characteristic.PositionState, this.platform.Characteristic.PositionState.STOPPED); + } } else { - this.service.updateCharacteristic(this.characteristic, value); + this.state = value; + if(this.service.getCharacteristic(this.characteristic).value != value){ + this.service.updateCharacteristic(this.characteristic, value); + } } } async updateIO(value: boolean){ - this.service.updateCharacteristic(this.platform.Characteristic.On, value); + this.state = value; + if(this.service.getCharacteristic(this.platform.Characteristic.On).value != value){ + this.service.updateCharacteristic(this.platform.Characteristic.On, value); + } } } diff --git a/src/device/input.ts b/src/device/input.ts index 8b5add3b..ec14076a 100644 --- a/src/device/input.ts +++ b/src/device/input.ts @@ -14,7 +14,7 @@ export class InputHandler { // set accessory information this.accessory.getService(this.platform.Service.AccessoryInformation)! .setCharacteristic(this.platform.Characteristic.Manufacturer, 'GCE-Electronic') - .setCharacteristic(this.platform.Characteristic.Model, 'IPX-800'); + .setCharacteristic(this.platform.Characteristic.Model, accessory.context.device.displayName); switch(accessory.context.device.type) { case 'motion': { @@ -38,7 +38,9 @@ export class InputHandler { this.service.setCharacteristic(this.platform.Characteristic.Name, accessory.context.device.displayName); } - public updateIO(state: boolean){ - this.service.updateCharacteristic(this.characteristic, state); + public updateIO(value: boolean){ + if(this.service.getCharacteristic(this.characteristic).value != value){ + this.service.updateCharacteristic(this.characteristic, value); + } } } diff --git a/src/device/relay.ts b/src/device/relay.ts index 768aee94..57484429 100644 --- a/src/device/relay.ts +++ b/src/device/relay.ts @@ -7,6 +7,7 @@ import { IpxApiCaller } from '../ipx/api'; export class RelayHandler { public readonly index: string = this.accessory.context.device.index; private readonly service: Service; + private state; constructor( private readonly platform: IPXPlatform, @@ -16,13 +17,17 @@ export class RelayHandler { // set accessory information this.accessory.getService(this.platform.Service.AccessoryInformation)! .setCharacteristic(this.platform.Characteristic.Manufacturer, 'GCE-Electronic') - .setCharacteristic(this.platform.Characteristic.Model, 'IPX-800'); + .setCharacteristic(this.platform.Characteristic.Model, accessory.context.device.displayName); switch(accessory.context.device.type) { case 'bswitch': { this.service = this.accessory.getService(this.platform.Service.Switch) || this.accessory.addService(this.platform.Service.Switch); break; } + case 'toggle': { + this.service = this.accessory.getService(this.platform.Service.Switch) || this.accessory.addService(this.platform.Service.Switch); + break; + } case 'switch': { this.service = this.accessory.getService(this.platform.Service.Switch) || this.accessory.addService(this.platform.Service.Switch); break; @@ -62,7 +67,14 @@ export class RelayHandler { } } - public updateIO(state: boolean){ - this.service.updateCharacteristic(this.platform.Characteristic.On, state); + public updateIO(value: boolean){ + if(this.accessory.context.device.type == 'toggle'){ + this.service.updateCharacteristic(this.platform.Characteristic.On, false); + }else{ + this.state = value; + if(this.service.getCharacteristic(this.platform.Characteristic.On).value != value){ + this.service.updateCharacteristic(this.platform.Characteristic.On, value); + } + } } } diff --git a/src/ipx/api.ts b/src/ipx/api.ts index 7db1cee5..aa082301 100644 --- a/src/ipx/api.ts +++ b/src/ipx/api.ts @@ -8,7 +8,6 @@ export abstract class IpxApiCaller { //public abstract setAnaPosition(value: CharacteristicValue, platform: IPXPlatform, accessory: PlatformAccessory): void; public abstract setVRPosition(value: CharacteristicValue, platform: IPXPlatform, accessory: PlatformAccessory): void; public abstract setDimmerPosition(value: CharacteristicValue, platform: IPXPlatform, accessory: PlatformAccessory): void; - public abstract getStateByDeviceIndex(platform: IPXPlatform): Promise>; - public abstract getAnaPositionByDeviceIndex(platform: IPXPlatform): Promise>; + public abstract getState(platform: IPXPlatform); } \ No newline at end of file diff --git a/src/ipx/ipxV4.ts b/src/ipx/ipxV4.ts index af511866..b2efef22 100644 --- a/src/ipx/ipxV4.ts +++ b/src/ipx/ipxV4.ts @@ -7,77 +7,196 @@ import MapUtils from '../utils'; export class IPXV4 implements IpxApiCaller { - async getStateByDeviceIndex(platform: IPXPlatform): Promise> { - const api = platform.config['api']; - const url = 'http://' + api.ip + '/api/xdevices.json?key=' + api.key + '&Get=all '; + public toVerify = {}; + public verifyTimeout; + + async getState(platform: IPXPlatform) { + let api = platform.config['api']; + const url = 'http://' + api.ip + '/api/xdevices.json?key=' + api.key + '&Get=all'; return axios.get(url).then(ipxInfo => { - const stateByIndex = new Map(); + this.verify(platform,ipxInfo.data); + + let io = new Map(); Object.keys(ipxInfo.data).map(key => { if (key.startsWith('R') || key.startsWith('V')) { - stateByIndex[key] = ipxInfo.data[key]; + io[key] = ipxInfo.data[key]; } else if (key.startsWith('G')) { - stateByIndex[key] = (ipxInfo.data[key]['Etat'] === 'ON'); + io[key] = (ipxInfo.data[key]['Etat'] === 'ON'); } }); - return stateByIndex; - }); - } - public getAnaPositionByDeviceIndex(platform: IPXPlatform): Promise> { - const api = platform.config['api']; - const url = 'http://' + api.ip + '/api/xdevices.json?key=' + api.key + '&Get=all '; - return axios.get(url).then(ipxInfo => { - const positionByIndex = new Map(); + let ana = new Map(); Object.keys(ipxInfo.data).map(key => { if (key.startsWith('G')) { - positionByIndex[key] = (ipxInfo.data[key]['Valeur']); + ana[key] = (ipxInfo.data[key]['Valeur']); } else if (key.startsWith('THL')) { - positionByIndex[key] = ipxInfo.data[key]; + ana[key] = ipxInfo.data[key]; + } else if (key.startsWith('VR')) { + let info = key.replace('VR','').split('-') + if(info.length == 2){ + let vrkey = 'VR'+String((parseInt(info[0])-1)*4+parseInt(info[1])).padStart(2, "0"); + ana[vrkey] = ipxInfo.data[key]; + } } }); - return positionByIndex; + + return {io : io, ana: ana} ; }); } - async setOnDimmer(value: CharacteristicValue, platform: IPXPlatform, accessory: PlatformAccessory) : Promise { + async setOnDimmer(value: CharacteristicValue, platform: IPXPlatform, accessory: PlatformAccessory) { if (value as boolean){ - return this.setDimmerPosition(101, platform, accessory); + this.setDimmerPosition(101, platform, accessory); } else { - return this.setDimmerPosition(0, platform, accessory); + this.setDimmerPosition(0, platform, accessory); } + return; } - async setOnRelay(value: CharacteristicValue, platform: IPXPlatform, accessory: PlatformAccessory) : Promise { - const onType = accessory.context.device.index.charAt(0).toUpperCase() === 'V' ? accessory.context.device.index.slice(0, 2).toUpperCase() : accessory.context.device.index.charAt(0).toUpperCase(); - const index = accessory.context.device.index.substring(onType.length); - const api = platform.config['api']; + async setOnRelay(value: CharacteristicValue, platform: IPXPlatform, accessory: PlatformAccessory) { + let onType = accessory.context.device.index.charAt(0).toUpperCase() === 'V' ? accessory.context.device.index.slice(0, 2).toUpperCase() : accessory.context.device.index.charAt(0).toUpperCase(); + let index = accessory.context.device.index.substring(onType.length); + let api = platform.config['api']; + if(accessory.context.device.type == 'toggle'){ + let url = 'http://' + api.ip + '/api/xdevices.json?key=' + api.key + '&Toggle' + onType + '=' + index; + platform.log.info(accessory.context.device.displayName + ' Toogle ---------- url: ' + url); + this.sendOrder(url,platform,0); + return; + } if (value as boolean){ - const url = 'http://' + api.ip + '/api/xdevices.json?key=' + api.key + '&Set' + onType + '=' + index; - platform.log.debug('v4------ '+ accessory.context.device.displayName + ' On ---------- url: ' + url); - return axios.get(url); + let url = 'http://' + api.ip + '/api/xdevices.json?key=' + api.key + '&Set' + onType + '=' + index; + platform.log.info(accessory.context.device.displayName + ' On ---------- url: ' + url); + this.sendOrder(url,platform,0); + this.toVerify[onType+index] = { + value: 1, + url: url, + datetime : Math.round(new Date().getTime()/1000) + } } else { - const url = 'http://' + api.ip + '/api/xdevices.json?key=' + api.key + '&Clear' + onType + '=' + index; - platform.log.debug('v4------ '+ accessory.context.device.displayName + ' Off ---------- url: ' + url); - return axios.get(url); + let url = 'http://' + api.ip + '/api/xdevices.json?key=' + api.key + '&Clear' + onType + '=' + index; + platform.log.info(accessory.context.device.displayName + ' Off ---------- url: ' + url); + this.sendOrder(url,platform,0); + this.toVerify[onType+index] = { + value: 0, + url: url, + datetime : Math.round(new Date().getTime()/1000) + } + } + return; + } + + async setVRPosition(value: CharacteristicValue, platform: IPXPlatform, accessory: PlatformAccessory) { + let nVal = 100 - Math.min(Math.max(value as number, 0), 100); + let api = platform.config['api']; + let url = 'http://' + api.ip + '/api/xdevices.json?key=' + api.key + '&Set' + accessory.context.device.index + '=' + nVal; + platform.log.info(accessory.context.device.displayName + ' ---------- on ' + url); + platform.log.info('Set Characteristic position -> ', nVal); + let loop = 0 + let self = this + let myInterval = setInterval(function(){ + loop++ + if(loop > 15){ + clearInterval(myInterval); + return; + } + self.getState(platform).then(state => { + if(state.ana[accessory.context.device.index] !== undefined && nVal == state.ana[accessory.context.device.index]){ + platform.updateDevices(); + clearInterval(myInterval); + return; + } + }) + },2000) + self.sendOrder(url,platform,0); + this.toVerify[accessory.context.device.index] = { + value: nVal, + url: url, + datetime : Math.round(new Date().getTime()/1000) } + return; } - async setVRPosition(value: CharacteristicValue, platform: IPXPlatform, accessory: PlatformAccessory): Promise { - const nVal = 100 - Math.min(Math.max(value as number, 0), 100); - const api = platform.config['api']; - const url = 'http://' + api.ip + '/api/xdevices.json?key=' + api.key + '&SetVR' + accessory.context.device.index + '=' + nVal; - platform.log.error('dimmer v4------ '+ accessory.context.device.displayName + ' ---------- on ' + url); - platform.log.debug('Set Characteristic Brightness -> ', nVal); - return axios.get(url); + async setDimmerPosition(value: CharacteristicValue, platform: IPXPlatform, accessory: PlatformAccessory) { + let nVal = Math.min(Math.max(value as number, 0), 100); + let api = platform.config['api']; + let index = Number(accessory.context.device.index.substring(1)); + let url = 'http://' + api.ip + '/api/xdevices.json?key=' + api.key + '&SetG' + ~~(index/5) + (index%5) + '=' + nVal; + platform.log.info(accessory.context.device.displayName + ' ---------- on ' + url); + this.sendOrder(url,platform,0); + return; } - async setDimmerPosition(value: CharacteristicValue, platform: IPXPlatform, accessory: PlatformAccessory): Promise { - const nVal = Math.min(Math.max(value as number, 0), 100); - const api = platform.config['api']; - const index = Number(accessory.context.device.index.substring(1)); - const url = 'http://' + api.ip + '/api/xdevices.json?key=' + api.key + '&SetG' + ~~(index/5) + (index%5) + '=' + nVal; - platform.log.debug('dimmer v4------ '+ accessory.context.device.displayName + ' ---------- on ' + url); - return axios.get(url); + public sendOrder(url,platform, retry){ + if(!retry){ + retry = 0; + } + retry++; + if(retry > 5){ + platform.log.error('Fail after 5 try on : '+url); + return; + } + platform.log.info('Call url -> ', url); + axios.get(url).then(response => { + if(response?.data?.status != 'Success'){ + platform.log.info('(Retry '+retry+') Error on : '+url+' result : ',response?.data); + setTimeout(() => { + this.sendOrder(url,platform,retry); + }, 100 * retry); + }else{ + platform.log.info('Succes on : '+url+' result : ',response?.data); + this.planVerify(platform,1250); + } + }).catch(error => { + platform.log.info('(Retry '+retry+') Error on : '+url); + setTimeout(() => { + this.sendOrder(url,platform,retry); + }, 100 * retry); + }); + } + + public verify(platform: IPXPlatform,ipxInfo){ + platform.log.info("Launch verify for "+JSON.stringify(this.toVerify)); + if(Object.keys(this.toVerify).length == 0){ + return; + } + for (const i in this.toVerify) { + if(!ipxInfo.hasOwnProperty(i)){ + delete this.toVerify[i] + continue; + } + if(ipxInfo[i] != this.toVerify[i].value){ + if(i.indexOf('VR') !== -1 && (this.toVerify[i].datetime + 30000) > Math.round(new Date().getTime()/1000)){ + platform.log.info(i+" => nok, value : "+ipxInfo[i]+" expected : "+this.toVerify[i].value+" but it's VR and it's too early I will wait little more"); + if(!this.verifyTimeout || this.verifyTimeout == -1){ + this.planVerify(platform,30000); + } + continue; + } + platform.log.info(i+" => nok, value : "+ipxInfo[i]+" expected : "+this.toVerify[i].value); + this.sendOrder(this.toVerify[i].url,platform,4); + } + delete this.toVerify[i] + }; + return; + } + + public planVerify(platform: IPXPlatform,timeout){ + if(!timeout){ + timeout = 1250 + } + if(this.verifyTimeout && this.verifyTimeout != -1){ + clearTimeout(this.verifyTimeout); + } + this.verifyTimeout = setTimeout(() => { + this.verifyTimeout = -1 + if(Object.keys(this.toVerify).length == 0){ + return; + } + platform.log.info("Launch verify from timeout"); + axios.get('http://' + platform.config['api'].ip + '/api/xdevices.json?key=' + platform.config['api'].key + '&Get=all').then(ipxInfo => { + this.verify(platform,ipxInfo.data); + }); + },timeout) } -} +} \ No newline at end of file diff --git a/src/ipx/ipxV5.ts b/src/ipx/ipxV5.ts index 929dfd55..76212c3c 100644 --- a/src/ipx/ipxV5.ts +++ b/src/ipx/ipxV5.ts @@ -7,59 +7,58 @@ import MapUtils from '../utils'; export class IPXV5 implements IpxApiCaller{ - async getStateByDeviceIndex(platform: IPXPlatform): Promise> { - const api = platform.config['api']; - const url = 'http://' + api.ip + '/api/core/io' + '?ApiKey=' + api.key ; - return axios.get(url) - .then(ipxInfo => MapUtils.toBoolByNum(ipxInfo.data, '_id', 'on')); - } - - async getAnaPositionByDeviceIndex(platform: IPXPlatform): Promise> { + async getState(platform: IPXPlatform) { const api = platform.config['api']; - const url = 'http://' + api.ip + '/api/core/ana' + '?ApiKey=' + api.key ; - return axios.get(url) - .then(ipxInfo => { - const res = MapUtils.toStringByNum(ipxInfo.data, '_id', 'value'); - return res; - }); + return axios.get('http://' + api.ip + '/api/core/ana' + '?ApiKey=' + api.key).then(ipxInfo => { + let ana = MapUtils.toStringByNum(ipxInfo.data, '_id', 'value'); + return axios.get('http://' + api.ip + '/api/core/io' + '?ApiKey=' + api.key).then(ipxInfo => { + let io = MapUtils.toBoolByNum(ipxInfo.data, '_id', 'on'); + return {io:io,ana:ana}; + }); + }); } - async setOnDimmer(value: CharacteristicValue, platform: IPXPlatform, accessory: PlatformAccessory) : Promise { - return this.setOnRelay(value, platform, accessory); + async setOnDimmer(value: CharacteristicValue, platform: IPXPlatform, accessory: PlatformAccessory) { + this.setOnRelay(value, platform, accessory); + return; } - async setOnRelay(value: CharacteristicValue, platform: IPXPlatform, accessory: PlatformAccessory) : Promise { + async setOnRelay(value: CharacteristicValue, platform: IPXPlatform, accessory: PlatformAccessory) { const api = platform.config['api']; const url = 'http://' + api.ip + '/api/core/io/' + accessory.context.device.index + '?ApiKey=' + api.key ; platform.log.debug('Set Characteristic On ->', value); if (value as boolean){ const json = JSON.stringify({ on: true }); platform.log.error('turning on '+ accessory.context.device.displayName + ' using ' + url + ' sending ' + json); - return axios.put(url, json); + axios.put(url, json); } else { const json = JSON.stringify({ on: false }); platform.log.error('turning off '+ accessory.context.device.displayName+ ' using ' + url + ' sending ' + json); - return axios.put(url, json); + axios.put(url, json); } + return; } - async setVRPosition(value: CharacteristicValue, platform: IPXPlatform, accessory: PlatformAccessory) : Promise { - return this.setAnaPosition(100 - (value as number), platform, accessory); + async setVRPosition(value: CharacteristicValue, platform: IPXPlatform, accessory: PlatformAccessory) { + this.setAnaPosition(100 - (value as number), platform, accessory); + return; } - async setDimmerPosition(value: CharacteristicValue, platform: IPXPlatform, accessory: PlatformAccessory) : Promise { - return this.setAnaPosition(100 - (value as number), platform, accessory); + async setDimmerPosition(value: CharacteristicValue, platform: IPXPlatform, accessory: PlatformAccessory) { + this.setAnaPosition(100 - (value as number), platform, accessory); + return; } - async setAnaPosition(value: CharacteristicValue, platform: IPXPlatform, accessory: PlatformAccessory) : Promise { + async setAnaPosition(value: CharacteristicValue, platform: IPXPlatform, accessory: PlatformAccessory) { const nVal = Math.min(Math.max(value as number, 0), 100); const api = platform.config['api']; const url = 'http://' + api.ip + '/api/core/ana/' + accessory.context.device.anaIndex + '?ApiKey=' + api.key ; const json = JSON.stringify({ virtual: true, value: nVal}); platform.log.error('setting level of '+ accessory.context.device.displayName + ' using ' + url + ' sending ' + json); platform.log.debug('Set Characteristic Brightness -> ', nVal); - return axios.put(url, json); + axios.put(url, json); + return; } } \ No newline at end of file diff --git a/src/platform.ts b/src/platform.ts index 636f1851..0fe7178b 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -11,6 +11,7 @@ import { RelayHandler } from './device/relay'; import { GradualHandler } from './device/gradual'; import { InputHandler } from './device/input'; import { AnalogInputHandler } from './device/analogInput'; +import { WebhookServer } from './webhookServer'; /** @@ -25,11 +26,14 @@ export class IPXPlatform implements DynamicPlatformPlugin { public readonly Characteristic: typeof Characteristic = this.homebridgeAPI.hap.Characteristic; private readonly configApi = this.config.api as Api; private readonly ipxApiCaller: IpxApiCaller = ((this.configApi.version === 'v5') ? new IPXV5: new IPXV4); + private readonly webhookServer: WebhookServer = new WebhookServer; // this is used to track restored cached accessories public readonly accessories: PlatformAccessory[] = []; + public ioDevices; + public anaDevices; private pullError = false; @@ -43,7 +47,23 @@ export class IPXPlatform implements DynamicPlatformPlugin { this.homebridgeAPI.on('didFinishLaunching', () => { log.debug('Executed didFinishLaunching callback'); // run the method to discover / register your devices as accessories - this.discoverDevices(); + let deviceConf = new DeviceConfReader(this.log, this.config); + let relays = deviceConf.relays.map(d => this.findOrCreate(d, (da) => new RelayHandler(this, da, this.ipxApiCaller))); + let graduals = deviceConf.graduals.map(d => this.findOrCreate(d, (da) => new GradualHandler(this, da, this.ipxApiCaller))) ; + let inputs = deviceConf.inputs.map(d => this.findOrCreate(d, (da) => new InputHandler(this, da))) ; + let anaInputs = deviceConf.anaInputs.map(d => this.findOrCreate(d, (da) => new AnalogInputHandler(this, da))) ; + this.ioDevices = relays.concat(graduals, inputs).filter(d => d.index); + this.anaDevices = graduals.concat(anaInputs); + this.webhookServer.start(this); + + this.updateDevices(); + + let self = this + if(this.config.api.pollInterval && this.config.api.pollInterval > 0){ + setInterval(function(){ + self.updateDevices(); + }, this.config.api.pollInterval * 1000); + } }); } @@ -52,53 +72,22 @@ export class IPXPlatform implements DynamicPlatformPlugin { * It should be used to setup event handlers for characteristics and update respective values. */ configureAccessory(device: PlatformAccessory) { - this.log.info('Loading accessory from cache:', device.displayName); - + let deviceConf = new DeviceConfReader(this.log, this.config); this.accessories.push(device); } - /** - * This is an example method showing how to register discovered accessories. - * Accessories must only be registered once, previously created accessories - * must not be registered again to prevent "duplicate UUID" errors. - */ - - discoverDevices() { - const deviceConf = new DeviceConfReader(this.log, this.config); - - const relays = deviceConf.relays.map(d => this.findOrCreate(d, (da) => new RelayHandler(this, da, this.ipxApiCaller))); - const graduals = deviceConf.graduals.map(d => this.findOrCreate(d, (da) => new GradualHandler(this, da, this.ipxApiCaller))) ; - const inputs = deviceConf.inputs.map(d => this.findOrCreate(d, (da) => new InputHandler(this, da))) ; - const anaInputs = deviceConf.anaInputs.map(d => this.findOrCreate(d, (da) => new AnalogInputHandler(this, da))) ; - - const ioDevices : Array = relays.concat(graduals, inputs).filter(d => d.index); - const anaDevices : Array = graduals.concat(anaInputs); - - //setInterval( () => { - this.ipxApiCaller.getStateByDeviceIndex(this) - .then(stateByIndex => { - Promise.all(ioDevices.map(d => { - if (stateByIndex[d.index.toUpperCase()] !== undefined) { - d.updateIO(stateByIndex[d.index.toUpperCase()]); + updateDevices() { + this.ipxApiCaller.getState(this) + .then(state => { + Promise.all(this.ioDevices.map(d => { + if (state.io[d.index.toUpperCase()] !== undefined) { + d.updateIO(state.io[d.index.toUpperCase()]); } })); - this.pullError = false; - }) - .catch(err => { - if (!this.pullError) { - this.log.error('could not update input/output devices state', err); - this.pullError = true; - } - }); - //}, 2930); - - //setInterval( () => { - this.ipxApiCaller.getAnaPositionByDeviceIndex(this) - .then(positionByIndex => { - Promise.all(anaDevices.map(d => { - const anaIndex = d.anaIndex || d.index; - if (positionByIndex[anaIndex.toUpperCase()] !== undefined) { - d.updateAnaValue(positionByIndex[anaIndex.toUpperCase()]); + Promise.all(this.anaDevices.map(d => { + let anaIndex = d.anaIndex || d.index; + if (state.ana[anaIndex.toUpperCase()] !== undefined) { + d.updateAnaValue(state.ana[anaIndex.toUpperCase()]); } })); this.pullError = false; @@ -108,26 +97,25 @@ export class IPXPlatform implements DynamicPlatformPlugin { this.log.error('could not update input/output devices state', err); this.pullError = true; } - }); - //}, 3553 ); + }); } + getDeviceUUID(device: Device){ + return this.homebridgeAPI.hap.uuid.generate(device.displayName.replace(/\s/g, '') + '-' + device.index); + } findOrCreate( device: Device, builder: (device: PlatformAccessory) => IODeviceHandler | AnaDeviceHandler, ):IODeviceHandler | AnaDeviceHandler { - const uuidSeed = device.displayName.replace(/\s/g, '') + '-' + device.index; - const uuid = this.homebridgeAPI.hap.uuid.generate(uuidSeed); - - const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid); - + let uuid = this.getDeviceUUID(device); + let existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid); if (existingAccessory) { this.log.info('Restoring existing accessory from cache:', existingAccessory.displayName); return builder(existingAccessory); } else { this.log.info('Adding new accessory:', device.displayName); - const accessory = new this.homebridgeAPI.platformAccessory(device.displayName, uuid); + let accessory = new this.homebridgeAPI.platformAccessory(device.displayName, uuid); accessory.context.device = device; this.homebridgeAPI.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); return builder(accessory); diff --git a/src/webhookServer.ts b/src/webhookServer.ts new file mode 100644 index 00000000..f8389505 --- /dev/null +++ b/src/webhookServer.ts @@ -0,0 +1,35 @@ +import http from 'http'; +import { IPXPlatform } from './platform'; + +export class WebhookServer { + + public updateTimeout; + + start(platform: IPXPlatform){ + if(!platform.config['api'].webhookPath || platform.config['api'].webhookPath.trim() == '' || !platform.config['api'].webhookPort){ + platform.log.info("Webhook configuration invalid"); + return; + } + const server = http.createServer() + server.listen(platform.config['api'].webhookPort) + platform.log.info("Started server for webhooks on port "+platform.config['api'].webhookPort); + server.on("request", (request, response) => { + platform.log.info("Received request"); + const { method, url, headers } = request + if (method === "GET" && url === "/"+platform.config['api'].webhookPath) { + if(this.updateTimeout && this.updateTimeout != -1){ + clearTimeout(this.updateTimeout); + } + this.updateTimeout = setTimeout(() => { + this.updateTimeout = -1 + platform.log.info("Update state"); + platform.updateDevices(); + },750) + } + response.statusCode = 200 + response.end() + }) + } + + +} \ No newline at end of file