From e7947dc769dabdf55478dde40709cff2e3f02221 Mon Sep 17 00:00:00 2001 From: JDierkse Date: Tue, 12 Jul 2022 16:34:41 +0200 Subject: [PATCH] Initial commit --- accessory.ts | 13 +++ device.ts | 12 +++ doorWindowSensorAccessory.ts | 134 ++++++++++++++++++++++++ environmentSensorAccessory.ts | 118 +++++++++++++++++++++ httpPromise.ts | 35 +++++++ httpServer.ts | 98 ++++++++++++++++++ index.ts | 8 ++ klikAanKlikUitAccessory.ts | 147 ++++++++++++++++++++++++++ motionSensorAccessory.ts | 111 ++++++++++++++++++++ platform.ts | 189 ++++++++++++++++++++++++++++++++++ platformAccessory.ts_ | 141 +++++++++++++++++++++++++ powerSocketAccessory.ts | 127 +++++++++++++++++++++++ powerSwitchAccessory.ts | 152 +++++++++++++++++++++++++++ settings.ts | 2 + 14 files changed, 1287 insertions(+) create mode 100644 accessory.ts create mode 100644 device.ts create mode 100644 doorWindowSensorAccessory.ts create mode 100644 environmentSensorAccessory.ts create mode 100644 httpPromise.ts create mode 100644 httpServer.ts create mode 100644 index.ts create mode 100644 klikAanKlikUitAccessory.ts create mode 100644 motionSensorAccessory.ts create mode 100644 platform.ts create mode 100644 platformAccessory.ts_ create mode 100644 powerSocketAccessory.ts create mode 100644 powerSwitchAccessory.ts create mode 100644 settings.ts diff --git a/accessory.ts b/accessory.ts new file mode 100644 index 0000000..d82995b --- /dev/null +++ b/accessory.ts @@ -0,0 +1,13 @@ +export abstract class accessory { + public type: string + public typeId: number + + constructor(type: string, typeId: number) { + this.type = type + this.typeId = typeId + } + + abstract setName(name) + abstract setValue(key, value) + abstract update() +} diff --git a/device.ts b/device.ts new file mode 100644 index 0000000..9b8d4ae --- /dev/null +++ b/device.ts @@ -0,0 +1,12 @@ +export interface device { +// --- Database Parameters --- + type: string + typeId: number +// --- HomeBridge Parameters --- + uniqueId: string + deviceName: string +// --- Extra Parameters --- + switchable: string + dimmable: string + detailedType: string +} diff --git a/doorWindowSensorAccessory.ts b/doorWindowSensorAccessory.ts new file mode 100644 index 0000000..d29e684 --- /dev/null +++ b/doorWindowSensorAccessory.ts @@ -0,0 +1,134 @@ +import { Service, PlatformAccessory, CharacteristicValue } from 'homebridge' + +import { accessory } from './accessory' +import { device } from './device' +import { httpRequest, response } from './httpPromise' +import { domoticaPlatform } from './platform' + +export class doorWindowSensorAccessory extends accessory { + private service: Service + + private state = { + Position: 0 + } + + static async discoverDevices(platform: domoticaPlatform): Promise { + return new Promise((resolve, reject) => { + let doorWindowSensorDevices: device[] = [] + + httpRequest("http://" + platform.config.hostname + ':' + platform.config.port + '/API/inventory/doorwindowsensor') + .then((response) => { + let devices = JSON.parse(response.body) + + for (const device of devices) { + if (device.enabled == "true") { + let object: device = { + type: "DoorWindowSensor", + typeId: device.id, + uniqueId: "DoorWindowSensor." + device.id, + deviceName: device.name, + switchable: "false", + dimmable: "false", + detailedType: device.type + } + doorWindowSensorDevices.push(object) + } + } + + resolve(doorWindowSensorDevices) + }) + .catch((error) => { + reject('doorWindowSensorAccessory::discoverDevices Error ->' + error) + }) + }) + } + + constructor( + private readonly platform: domoticaPlatform, + private readonly accessory: PlatformAccessory, + ) { + super("DoorWindowSensor", accessory.context.device.typeId) + this.accessory.getService(this.platform.Service.AccessoryInformation)! + .setCharacteristic(this.platform.Characteristic.Manufacturer, 'Xiaomi') + .setCharacteristic(this.platform.Characteristic.Model, 'MCCGQ01LM') + .setCharacteristic(this.platform.Characteristic.SerialNumber, '04000' + accessory.context.device.typeId) + + if (accessory.context.device.detailedType == "door") { + this.service = this.accessory.getService(this.platform.Service.Door) || this.accessory.addService(this.platform.Service.Door) + } else { // window + this.service = this.accessory.getService(this.platform.Service.Window) || this.accessory.addService(this.platform.Service.Window) + } + + this.service.getCharacteristic(this.platform.Characteristic.CurrentPosition) + .onGet(this.getCurrentPosition.bind(this)); + + this.service.getCharacteristic(this.platform.Characteristic.PositionState) + .onGet(this.getPositionState.bind(this)); + + this.service.getCharacteristic(this.platform.Characteristic.TargetPosition) + .onGet(this.getTargetPosition.bind(this)) + + this.setName(accessory.context.device.deviceName) + this.update() + } + + setName(name) { + this.service.setCharacteristic(this.platform.Characteristic.Name, name) + } + + setValue(key, value) { + if (key == "state" && value != "") { + if (value == "Off") { + this.state.Position = 0 + } else { + this.state.Position = 100 + } + this.service.updateCharacteristic(this.platform.Characteristic.CurrentPosition, this.state.Position) + this.service.updateCharacteristic(this.platform.Characteristic.TargetPosition, this.state.Position) + } + } + + update() { + const data = JSON.stringify([{ + type: 'doorwindow', + query: 'state' + }]) + + httpRequest("http://" + this.platform.config.hostname + ':' + this.platform.config.port + '/API/Status', data) + .then((response) => { + const devices = JSON.parse(response.body) + + for (const device of devices.doorwindow) { + if (device.id == this.accessory.context.device.typeId) { + if (device.state == 'closed') { + this.state.Position = 0 + } else { // open + this.state.Position = 100 + } + this.service.updateCharacteristic(this.platform.Characteristic.CurrentPosition, this.state.Position) + this.service.updateCharacteristic(this.platform.Characteristic.TargetPosition, this.state.Position) + break + } + } + }) + .catch((error) => { + this.platform.log.debug('doorWindowSensorAccessory::update Error ->' + error) + }) + } + + getCurrentPosition(): CharacteristicValue { + return this.state.Position + } + + getPositionState(): CharacteristicValue { + return this.platform.Characteristic.PositionState.STOPPED; + } + + getTargetPosition(): CharacteristicValue { + return this.state.Position + } + + getPosition(): CharacteristicValue { + return this.state.Position + } +} diff --git a/environmentSensorAccessory.ts b/environmentSensorAccessory.ts new file mode 100644 index 0000000..5a74b58 --- /dev/null +++ b/environmentSensorAccessory.ts @@ -0,0 +1,118 @@ +import { Service, PlatformAccessory, CharacteristicValue } from 'homebridge' + +import { accessory } from './accessory' +import { device } from './device' +import { httpRequest, response } from './httpPromise' +import { domoticaPlatform } from './platform' + +export class environmentSensorAccessory extends accessory { + private serviceTemperature: Service + private serviceHumidity: Service + + private state = { + Temperature: 0, + Humidity: 0 + } + + static async discoverDevices(platform: domoticaPlatform): Promise { + return new Promise((resolve, reject) => { + let environmentSensorDevices: device[] = [] + + httpRequest("http://" + platform.config.hostname + ':' + platform.config.port + '/API/inventory/environmentsensor') + .then((response) => { + let devices = JSON.parse(response.body) + + for (const device of devices) { + if (device.enabled == "true") { + let object: device = { + type: "EnvironmentSensor", + typeId: device.id, + uniqueId: "EnvironmentSensor." + device.id, + deviceName: device.name, + switchable: "false", + dimmable: "false", + detailedType: "" + } + environmentSensorDevices.push(object) + } + } + + resolve(environmentSensorDevices) + }) + .catch((error) => { + reject('environmentSensorAccessory::discoverDevices Error ->' + error) + }) + }) + } + + constructor( + private readonly platform: domoticaPlatform, + private readonly accessory: PlatformAccessory, + ) { + super("EnvironmentSensor", accessory.context.device.typeId) + this.accessory.getService(this.platform.Service.AccessoryInformation)! + .setCharacteristic(this.platform.Characteristic.Manufacturer, 'Xiaomi') + .setCharacteristic(this.platform.Characteristic.Model, 'WSDCGQ01LM') + .setCharacteristic(this.platform.Characteristic.SerialNumber, '03000' + accessory.context.device.typeId) + + this.serviceTemperature = this.accessory.getService(this.platform.Service.TemperatureSensor) || this.accessory.addService(this.platform.Service.TemperatureSensor) + this.serviceTemperature.getCharacteristic(this.platform.Characteristic.CurrentTemperature) + .onGet(this.getTemperature.bind(this)) + + this.serviceHumidity = this.accessory.getService(this.platform.Service.HumiditySensor) || this.accessory.addService(this.platform.Service.HumiditySensor) + this.serviceHumidity.getCharacteristic(this.platform.Characteristic.CurrentRelativeHumidity) + .onGet(this.getHumidity.bind(this)) + + this.setName(accessory.context.device.deviceName) + this.update() + } + + setName(name) { + this.serviceTemperature.setCharacteristic(this.platform.Characteristic.Name, name) + this.serviceHumidity.setCharacteristic(this.platform.Characteristic.Name, name) + } + + setValue(key, value) { + if (key == "temperature" && value != "") { + this.state.Temperature = value / 100 + this.serviceTemperature.updateCharacteristic(this.platform.Characteristic.CurrentTemperature, this.state.Temperature) + } + if (key == "humidity" && value != "") { + this.state.Humidity = value / 100 + this.serviceHumidity.updateCharacteristic(this.platform.Characteristic.CurrentRelativeHumidity, this.state.Humidity) + } + } + + update() { + const data = JSON.stringify([{ + type: 'environment', + query: 'state' + }]) + + httpRequest("http://" + this.platform.config.hostname + ':' + this.platform.config.port + '/API/Status', data) + .then((response) => { + const devices = JSON.parse(response.body) + + for (const device of devices.environment) { + if (device.id == this.accessory.context.device.typeId) { + this.state.Temperature = device.temperature + this.state.Humidity = device.humidity + this.serviceTemperature.updateCharacteristic(this.platform.Characteristic.CurrentTemperature, this.state.Temperature) + this.serviceHumidity.updateCharacteristic(this.platform.Characteristic.CurrentRelativeHumidity, this.state.Humidity) + break + } + } + }) + .catch((error) => { + this.platform.log.debug('environmentSensorAccessory::getTemperature Error ->' + error) + }) + } + + getTemperature(): CharacteristicValue { + return this.state.Temperature + } + + getHumidity(): CharacteristicValue { + return this.state.Humidity + } +} diff --git a/httpPromise.ts b/httpPromise.ts new file mode 100644 index 0000000..f4518b3 --- /dev/null +++ b/httpPromise.ts @@ -0,0 +1,35 @@ +'use strict' + +const http = require('http') +const https = require('https') + +export interface response { + statusCode: number, + headers: object, + body: string +} + +export function httpRequest(urlOptions, data = ''): Promise { + let operator = http + if (urlOptions.startsWith('https')) + operator = https + + return new Promise((resolve, reject) => { + const request = operator.request(urlOptions, + (result) => { + let body = '' + result.on('data', (chunk = '') => (body += chunk)) + result.on('error', reject) + result.on('end', () => { + if (result.statusCode >= 200 && result.statusCode <= 299) { + resolve({statusCode: result.statusCode, headers: result.headers, body: body}) + } else { + reject('Request failed. status: ' + result.statusCode + ', body: ' + body) + } + }) + }) + request.on('error', reject) + request.write(data, 'binary') + request.end() + }) +} diff --git a/httpServer.ts b/httpServer.ts new file mode 100644 index 0000000..a7b43aa --- /dev/null +++ b/httpServer.ts @@ -0,0 +1,98 @@ +import { PlatformConfig } from 'homebridge' + +import { domoticaPlatform } from './platform' + +const request = require("request") +const http = require('http') +const url = require('url') + +export class httpServer { + private address + private port + + constructor( + private readonly platform: domoticaPlatform, + private readonly config: PlatformConfig + ) { + this.address = "0.0.0.0" + this.port = 8080 + } + + serverCallback (request, response) { + var urlObject = url.parse(request.url, true) + var urlQuery = urlObject.query + var body: Uint8Array[] = [] + var log = this.platform.log + + request.on('error', (err) => { + log.debug("[ERROR Homebridge Domotica HttpServer] Reason: %s.", err) + }).on('data', (chunk) => { + body.push(chunk) + }).on('end', () => { + var bodyString = Buffer.concat(body).toString() + + response.on('error', function(err) { + log.debug("[ERROR Homebridge Domotica HttpServer] Reason: %s.", err) + }) + + response.statusCode = 200 + response.setHeader('Content-Type', 'application/json') + + if (!urlQuery.type || !urlQuery.typeId) { + response.statusCode = 404 + response.setHeader("Content-Type", "text/plain") + var errorText = "[ERROR Homebridge Domotica HttpServer] Type or TypeId missing in request." + response.write(errorText) + response.end() + } else { + var type = urlQuery.type + var typeId = urlQuery.typeId + + if (type != "PowerSocket" && type != "PowerSwitch" && type != "Xiaomi") { + log.debug(request.url) + } + + if (type == "Xiaomi") { + type = urlQuery.model + if (type == "TemperatureHumiditySensor") { + type = "EnvironmentSensor" + } + } + + var foundAccessory + + for (const accessory of this.platform.accessories) { + if (accessory.type == type && accessory.typeId == typeId) { + foundAccessory = accessory + break + } + } + + if (foundAccessory) { + for (const key of Object.keys(urlQuery)) { + if (key != "type" && key != "typeId") { + foundAccessory.setValue(key, urlQuery[key]) + } + } + + var responseBody = { "success" : true }; + + response.write(JSON.stringify(responseBody)) + response.end() + } else { + response.setHeader("Content-Type", "text/plain") + errorText = "[ERROR Homebridge Domotica HttpServer] " + type + " " + typeId + " not found." + if (type != "PowerSocket") { + log.debug(errorText + '(' + request.url + ')') + } + response.write(errorText) + response.end() + } + } + }) + } + + start() { + http.createServer(this.serverCallback.bind(this)).listen(this.port, this.address) + } +} diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..853a571 --- /dev/null +++ b/index.ts @@ -0,0 +1,8 @@ +import { API } from 'homebridge' + +import { PLATFORM_NAME } from './settings' +import { domoticaPlatform } from './platform' + +export = (api: API) => { + api.registerPlatform(PLATFORM_NAME, domoticaPlatform) +} diff --git a/klikAanKlikUitAccessory.ts b/klikAanKlikUitAccessory.ts new file mode 100644 index 0000000..c2b6d89 --- /dev/null +++ b/klikAanKlikUitAccessory.ts @@ -0,0 +1,147 @@ +import { Service, PlatformAccessory, CharacteristicValue } from 'homebridge' + +import { accessory } from './accessory' +import { device } from './device' +import { httpRequest, response } from './httpPromise' +import { domoticaPlatform } from './platform' + +export class klikAanKlikUitAccessory extends accessory { + private service: Service + + private state = { + On: false, + Brightness: 100 + } + + static async discoverDevices(platform: domoticaPlatform): Promise { + return new Promise((resolve, reject) => { + let klikaanklikuitDevices: device[] = [] + + httpRequest("http://" + platform.config.hostname + ':' + platform.config.port + '/API/inventory/klikaanklikuit') + .then((response) => { + let devices = JSON.parse(response.body) + + for (const device of devices) { + if (device.enabled == "true") { + let object: device = { + type: "KlikAanKlikUit", + typeId: device.id, + uniqueId: "KlikAanKlikUit." + device.id, + deviceName: device.name, + switchable: "true", + dimmable: device.dimmable, + detailedType: "" + } + klikaanklikuitDevices.push(object) + } + } + + resolve(klikaanklikuitDevices) + }) + .catch((error) => { + reject('klikAanKlikUitAccessory::discoverDevices Error ->' + error) + }) + }) + } + + constructor( + private readonly platform: domoticaPlatform, + private readonly accessory: PlatformAccessory, + ) { + super("KlikAanKlikUit", accessory.context.device.typeId) + this.accessory.getService(this.platform.Service.AccessoryInformation)! + .setCharacteristic(this.platform.Characteristic.Manufacturer, 'KlikAanKlikUit') + .setCharacteristic(this.platform.Characteristic.Model, 'AC-1000') + .setCharacteristic(this.platform.Characteristic.SerialNumber, '01000' + accessory.context.device.typeId) + + this.service = this.accessory.getService(this.platform.Service.Lightbulb) || this.accessory.addService(this.platform.Service.Lightbulb) + this.service.getCharacteristic(this.platform.Characteristic.On) + .onSet(this.setOn.bind(this)) + .onGet(this.getOn.bind(this)) + + if (accessory.context.device.dimmable != "false") { // WTF? + this.service.getCharacteristic(this.platform.Characteristic.Brightness) + .onSet(this.setBrightness.bind(this)) + .onGet(this.getBrightness.bind(this)) + } + + this.setName(accessory.context.device.deviceName) + this.update() + } + + + setName(name) { + this.service.setCharacteristic(this.platform.Characteristic.Name, name) + } + + setValue(key, value) { + if (key == "state" && value != "") { + this.state.On = (value == "on") + this.service.updateCharacteristic(this.platform.Characteristic.On, this.state.On) + } else if (key == "dimLevel" && value!= "") { + this.state.Brightness = value + this.service.updateCharacteristic(this.platform.Characteristic.Brightness, this.state.Brightness) + } + } + + update() { + const data = JSON.stringify([{ + type: 'klikaanklikuit', + query: 'state' + }]) + + httpRequest("http://" + this.platform.config.hostname + ':' + this.platform.config.port + '/API/Status', data) + .then((response) => { + const devices = JSON.parse(response.body) + + for (const device of devices.klikaanklikuit) { + if (device.id == this.accessory.context.device.typeId) { + if (device.state == 'on') { + this.state.On = true + } else { + this.state.On = false + } + this.service.updateCharacteristic(this.platform.Characteristic.On, this.state.On) + + if (device.dimmable != 'false') { // WTF? + this.state.Brightness = device.dimLevel; + this.service.updateCharacteristic(this.platform.Characteristic.Brightness, this.state.Brightness) + } + + break + } + } + }) + .catch((error) => { + this.platform.log.debug('klikAanKlikUitAccessory::update Error ->' + error) + }) + } + + async setOn(value: CharacteristicValue) { + this.state.On = value as boolean + + // http://$domoticaIP/external/WebUpdate.php?data=KlikAanKlikUit/$id/$state/ + httpRequest("http://" + this.platform.config.hostname + '/external/WebUpdate.php?data=KlikAanKlikUit/' + this.accessory.context.device.typeId + '/' + (this.state.On? '1': '0') + '/') + .catch((error) => { + this.platform.log.debug('klikAanKlikUitAccessory::setOn Error ->' + error) + }) + } + + getOn(): CharacteristicValue { + return this.state.On + } + + async setBrightness(value: CharacteristicValue) { + this.state.Brightness = value as number + + // http://$domoticaIP/external/WebUpdate.php?data=KlikAanKlikUit/$id/$state/$dimLevel + httpRequest("http://" + this.platform.config.hostname + '/external/WebUpdate.php?data=KlikAanKlikUit/' + this.accessory.context.device.typeId + '//' + this.state.Brightness) + .catch((error) => { + this.platform.log.debug('klikAanKlikUitAccessory::setBrightness Error ->' + error) + }) + } + + getBrightness(): CharacteristicValue { + return this.state.Brightness + } +} diff --git a/motionSensorAccessory.ts b/motionSensorAccessory.ts new file mode 100644 index 0000000..2c9e947 --- /dev/null +++ b/motionSensorAccessory.ts @@ -0,0 +1,111 @@ +import { Service, PlatformAccessory, CharacteristicValue } from 'homebridge' + +import { accessory } from './accessory' +import { device } from './device' +import { httpRequest, response } from './httpPromise' +import { domoticaPlatform } from './platform' + +export class motionSensorAccessory extends accessory { + private service: Service + + private state = { + Motion: false + } + + static async discoverDevices(platform: domoticaPlatform): Promise { + return new Promise((resolve, reject) => { + let motionSensorDevices: device[] = [] + + httpRequest("http://" + platform.config.hostname + ':' + platform.config.port + '/API/inventory/motionsensor') + .then((response) => { + let devices = JSON.parse(response.body) + + for (const device of devices) { + if (device.enabled == "true") { + let object: device = { + type: "MotionSensor", + typeId: device.id, + uniqueId: "MotionSensor." + device.id, + deviceName: device.name, + switchable: "false", + dimmable: "false", + detailedType: device.type + } + motionSensorDevices.push(object) + } + } + + resolve(motionSensorDevices) + }) + .catch((error) => { + reject('motionSensorAccessory::discoverDevices Error ->' + error) + }) + }) + } + + constructor( + private readonly platform: domoticaPlatform, + private readonly accessory: PlatformAccessory, + ) { + super("MotionSensor", accessory.context.device.typeId) + this.accessory.getService(this.platform.Service.AccessoryInformation)! + .setCharacteristic(this.platform.Characteristic.Manufacturer, 'Xiaomi') + .setCharacteristic(this.platform.Characteristic.Model, 'RTCGQ01LM') + .setCharacteristic(this.platform.Characteristic.SerialNumber, '05000' + accessory.context.device.typeId) + + this.service = this.accessory.getService(this.platform.Service.MotionSensor) || this.accessory.addService(this.platform.Service.MotionSensor) + + this.service.getCharacteristic(this.platform.Characteristic.MotionDetected) + .onGet(this.getState.bind(this)); + + this.setName(accessory.context.device.deviceName) + this.update() + } + + setName(name) { + this.service.setCharacteristic(this.platform.Characteristic.Name, name) + } + + setValue(key, value) { + if (key == "state" && value != "") { + if (value == "On") { + this.state.Motion = true + } else { + this.state.Motion = false + } + this.service.updateCharacteristic(this.platform.Characteristic.MotionDetected, this.state.Motion) + } + } + + update() { + const data = JSON.stringify([{ + type: 'motion', + query: 'state' + }]) + + httpRequest("http://" + this.platform.config.hostname + ':' + this.platform.config.port + '/API/Status', data) + .then((response) => { + const devices = JSON.parse(response.body) + + for (const device of devices.motion) { + if (device.id == this.accessory.context.device.typeId) { + if (device.state == 'absent') { + this.state.Motion = false + } else { // present + this.state.Motion = true + } + return this.state.Motion + this.service.updateCharacteristic(this.platform.Characteristic.MotionDetected, this.state.Motion) + break + } + } + }) + .catch((error) => { + this.platform.log.debug('motionSensorAccessory::update Error ->' + error) + }) + } + + getState(): CharacteristicValue { + return this.state.Motion + } +} diff --git a/platform.ts b/platform.ts new file mode 100644 index 0000000..418fbbe --- /dev/null +++ b/platform.ts @@ -0,0 +1,189 @@ +import { API, DynamicPlatformPlugin, Logger, PlatformAccessory, PlatformConfig, Service, Characteristic } from 'homebridge' + +import { PLATFORM_NAME, PLUGIN_NAME } from './settings' + +import { accessory } from './accessory' +import { device } from './device' +import { klikAanKlikUitAccessory } from './klikAanKlikUitAccessory' +import { powerSocketAccessory } from './powerSocketAccessory' +import { powerSwitchAccessory } from './powerSwitchAccessory' +import { environmentSensorAccessory } from './environmentSensorAccessory' +import { doorWindowSensorAccessory } from './doorWindowSensorAccessory' +import { motionSensorAccessory } from './motionSensorAccessory' +import { httpServer } from './httpServer' + +export class domoticaPlatform implements DynamicPlatformPlugin { + public readonly Service: typeof Service = this.api.hap.Service + public readonly Characteristic: typeof Characteristic = this.api.hap.Characteristic + + public readonly accessories: accessory[] = [] + public readonly cachedAccessories: PlatformAccessory[] = [] + public readonly presentAccessories: PlatformAccessory[] = [] + + private readonly discoveries: String[] = [] + private readonly completedDiscoveries: String[] = [] + + private httpServer: httpServer = new httpServer(this, this.config) + + constructor( + public readonly log: Logger, + public readonly config: PlatformConfig, + public readonly api: API + ) { + this.api.on('didFinishLaunching', () => { + this.discoverDevices() + this.httpServer.start() + }) + } + + configureAccessory(accessory: PlatformAccessory) { + this.cachedAccessories.push(accessory) + } + + discoverDevices() { + this.discoveryStarted('klikAanKlikUitAccessory') + klikAanKlikUitAccessory.discoverDevices(this) + .then((devices) => { + this.processDiscoveredDevices(devices) + this.discoveryCompleted('klikAanKlikUitAccessory') + }) + .catch((error) => { + this.log.debug('klikAanKlikUitAccessory.discoverDevices Error ->' + error) + }) + + this.discoveryStarted('powerSocketAccessory') + powerSocketAccessory.discoverDevices(this) + .then((devices) => { + this.processDiscoveredDevices(devices) + this.discoveryCompleted('powerSocketAccessory') + }) + .catch((error) => { + this.log.debug('powerSocketAccessory.discoverDevices Error ->' + error) + }) + + this.discoveryStarted('powerSwitchAccessory') + powerSwitchAccessory.discoverDevices(this) + .then((devices) => { + this.processDiscoveredDevices(devices) + this.discoveryCompleted('powerSwitchAccessory') + }) + .catch((error) => { + this.log.debug('powerSwitchAccessory.discoverDevices Error ->' + error) + }) + + this.discoveryStarted('environmentSensorAccessory') + environmentSensorAccessory.discoverDevices(this) + .then((devices) => { + this.processDiscoveredDevices(devices) + this.discoveryCompleted('environmentSensorAccessory') + }) + .catch((error) => { + this.log.debug('environmentSensorAccessory.discoverDevices Error ->' + error) + }) + + this.discoveryStarted('doorWindowSensorAccessory') + doorWindowSensorAccessory.discoverDevices(this) + .then((devices) => { + this.processDiscoveredDevices(devices) + this.discoveryCompleted('doorWindowSensorAccessory') + }) + .catch((error) => { + this.log.debug('doorWindowSensorAccessory.discoverDevices Error ->' + error) + }) + + this.discoveryStarted('motionSensorAccessory') + motionSensorAccessory.discoverDevices(this) + .then((devices) => { + this.processDiscoveredDevices(devices) + this.discoveryCompleted('motionSensorAccessory') + }) + .catch((error) => { + this.log.debug('motionSensorAccessory.discoverDevices Error ->' + error) + }) + } + + processDiscoveredDevices(domoticaDevices: device[]) { + for (const device of domoticaDevices) { + const uuid = this.api.hap.uuid.generate(device.uniqueId) + const existingAccessory = this.cachedAccessories.find(accessory => accessory.UUID === uuid) + + if (existingAccessory) { + this.log.info('| ' + existingAccessory.context.device.type + ': ' + existingAccessory.displayName) + + this.presentAccessories.push(existingAccessory) + + if (existingAccessory.displayName != device.deviceName) { + this.log.info('! ' + existingAccessory.displayName + ' => ' + device.deviceName) + existingAccessory.context.device = device + this.api.updatePlatformAccessories([existingAccessory]) + } + + if (existingAccessory.context.device.type == 'KlikAanKlikUit') { + this.accessories.push(new klikAanKlikUitAccessory(this, existingAccessory)) + } else if (existingAccessory.context.device.type == 'PowerSocket') { + this.accessories.push(new powerSocketAccessory(this, existingAccessory)) + } else if (existingAccessory.context.device.type == 'PowerSwitch') { + this.accessories.push(new powerSwitchAccessory(this, existingAccessory)) + } else if (existingAccessory.context.device.type == 'EnvironmentSensor') { + this.accessories.push(new environmentSensorAccessory(this, existingAccessory)) + } else if (existingAccessory.context.device.type == 'DoorWindowSensor') { + this.accessories.push(new doorWindowSensorAccessory(this, existingAccessory)) + } else if (existingAccessory.context.device.type == 'MotionSensor') { + this.accessories.push(new motionSensorAccessory(this, existingAccessory)) + } + } else { + this.log.info('+ ' + device.type + ': ' + device.deviceName) + + const accessory = new this.api.platformAccessory(device.deviceName, uuid) + accessory.context.device = device + + if (accessory.context.device.type == 'KlikAanKlikUit') { + this.accessories.push(new klikAanKlikUitAccessory(this, accessory)) + } else if (accessory.context.device.type == 'PowerSocket') { + this.accessories.push(new powerSocketAccessory(this, accessory)) + } else if (accessory.context.device.type == 'PowerSwitch') { + this.accessories.push(new powerSwitchAccessory(this, accessory)) + } else if (accessory.context.device.type == 'EnvironmentSensor') { + this.accessories.push(new environmentSensorAccessory(this, accessory)) + } else if (accessory.context.device.type == 'DoorWindowSensor') { + this.accessories.push(new doorWindowSensorAccessory(this, accessory)) + } else if (accessory.context.device.type == 'MotionSensor') { + this.accessories.push(new motionSensorAccessory(this, accessory)) + } + + this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]) + } + } + } + + discoveryStarted(deviceType) { + this.discoveries.push(deviceType) + } + + discoveryCompleted(deviceType) { + this.completedDiscoveries.push(deviceType) + + if (this.completedDiscoveries.length == this.discoveries.length) { + this.cleanupRemovedDevices() + } + + //setInterval(this.updateAccessories.bind(this), 60 * 1000); // Poll every minute + } + + updateAccessories() { + for (const accessory of this.accessories) { + accessory.update() + } + } + + cleanupRemovedDevices() { + for (const existingAccessory of this.cachedAccessories) { + const presentAccessory = this.presentAccessories.find(accessory => accessory.UUID === existingAccessory.UUID) + + if (!presentAccessory) { + this.log.info('- ' + existingAccessory.context.device.type + ': ' + existingAccessory.displayName) + this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [existingAccessory]) + } + } + } +} diff --git a/platformAccessory.ts_ b/platformAccessory.ts_ new file mode 100644 index 0000000..2846de6 --- /dev/null +++ b/platformAccessory.ts_ @@ -0,0 +1,141 @@ +import { Service, PlatformAccessory, CharacteristicValue } from 'homebridge' + +import { ExampleHomebridgePlatform } from './platform' + +/** + * Platform Accessory + * An instance of this class is created for each accessory your platform registers + * Each accessory may expose multiple services of different service types. + */ +export class ExamplePlatformAccessory { + private service: Service + + /** + * These are just used to create a working example + * You should implement your own code to track the state of your accessory + */ + private exampleStates = { + On: false, + Brightness: 100, + } + + constructor( + private readonly platform: ExampleHomebridgePlatform, + private readonly accessory: PlatformAccessory, + ) { + + // set accessory information + this.accessory.getService(this.platform.Service.AccessoryInformation)! + .setCharacteristic(this.platform.Characteristic.Manufacturer, 'Default-Manufacturer') + .setCharacteristic(this.platform.Characteristic.Model, 'Default-Model') + .setCharacteristic(this.platform.Characteristic.SerialNumber, 'Default-Serial') + + // get the LightBulb service if it exists, otherwise create a new LightBulb service + // you can create multiple services for each accessory + this.service = this.accessory.getService(this.platform.Service.Lightbulb) || this.accessory.addService(this.platform.Service.Lightbulb) + + // set the service name, this is what is displayed as the default name on the Home app + // in this example we are using the name we stored in the `accessory.context` in the `discoverDevices` method. + this.service.setCharacteristic(this.platform.Characteristic.Name, accessory.context.device.exampleDisplayName) + + // each service must implement at-minimum the "required characteristics" for the given service type + // see https://developers.homebridge.io/#/service/Lightbulb + + // register handlers for the On/Off Characteristic + this.service.getCharacteristic(this.platform.Characteristic.On) + .onSet(this.setOn.bind(this)) // SET - bind to the `setOn` method below + .onGet(this.getOn.bind(this)) // GET - bind to the `getOn` method below + + // register handlers for the Brightness Characteristic + this.service.getCharacteristic(this.platform.Characteristic.Brightness) + .onSet(this.setBrightness.bind(this)) // SET - bind to the 'setBrightness` method below + + /** + * Creating multiple services of the same type. + * + * To avoid "Cannot add a Service with the same UUID another Service without also defining a unique 'subtype' property." error, + * when creating multiple services of the same type, you need to use the following syntax to specify a name and subtype id: + * this.accessory.getService('NAME') || this.accessory.addService(this.platform.Service.Lightbulb, 'NAME', 'USER_DEFINED_SUBTYPE_ID') + * + * The USER_DEFINED_SUBTYPE must be unique to the platform accessory (if you platform exposes multiple accessories, each accessory + * can use the same sub type id.) + */ + + // Example: add two "motion sensor" services to the accessory + const motionSensorOneService = this.accessory.getService('Motion Sensor One Name') || + this.accessory.addService(this.platform.Service.MotionSensor, 'Motion Sensor One Name', 'YourUniqueIdentifier-1') + + const motionSensorTwoService = this.accessory.getService('Motion Sensor Two Name') || + this.accessory.addService(this.platform.Service.MotionSensor, 'Motion Sensor Two Name', 'YourUniqueIdentifier-2') + + /** + * Updating characteristics values asynchronously. + * + * Example showing how to update the state of a Characteristic asynchronously instead + * of using the `on('get')` handlers. + * Here we change update the motion sensor trigger states on and off every 10 seconds + * the `updateCharacteristic` method. + * + */ + let motionDetected = false + setInterval(() => { + // EXAMPLE - inverse the trigger + motionDetected = !motionDetected + + // push the new value to HomeKit + motionSensorOneService.updateCharacteristic(this.platform.Characteristic.MotionDetected, motionDetected) + motionSensorTwoService.updateCharacteristic(this.platform.Characteristic.MotionDetected, !motionDetected) + + this.platform.log.debug('Triggering motionSensorOneService:', motionDetected) + this.platform.log.debug('Triggering motionSensorTwoService:', !motionDetected) + }, 10000) + } + + /** + * Handle "SET" requests from HomeKit + * These are sent when the user changes the state of an accessory, for example, turning on a Light bulb. + */ + async setOn(value: CharacteristicValue) { + // implement your own code to turn your device on/off + this.exampleStates.On = value as boolean + + this.platform.log.debug('Set Characteristic On ->', value) + } + + /** + * Handle the "GET" requests from HomeKit + * These are sent when HomeKit wants to know the current state of the accessory, for example, checking if a Light bulb is on. + * + * GET requests should return as fast as possbile. A long delay here will result in + * HomeKit being unresponsive and a bad user experience in general. + * + * If your device takes time to respond you should update the status of your device + * asynchronously instead using the `updateCharacteristic` method instead. + + * @example + * this.service.updateCharacteristic(this.platform.Characteristic.On, true) + */ + async getOn(): Promise { + // implement your own code to check if the device is on + const isOn = this.exampleStates.On + + this.platform.log.debug('Get Characteristic On ->', isOn) + + // if you need to return an error to show the device as "Not Responding" in the Home app: + // throw new this.platform.api.hap.HapStatusError(this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE) + + return isOn + } + + /** + * Handle "SET" requests from HomeKit + * These are sent when the user changes the state of an accessory, for example, changing the Brightness + */ + async setBrightness(value: CharacteristicValue) { + // implement your own code to set the brightness + this.exampleStates.Brightness = value as number + + this.platform.log.debug('Set Characteristic Brightness -> ', value) + } + +} diff --git a/powerSocketAccessory.ts b/powerSocketAccessory.ts new file mode 100644 index 0000000..9bfbefd --- /dev/null +++ b/powerSocketAccessory.ts @@ -0,0 +1,127 @@ +import { Service, PlatformAccessory, CharacteristicValue } from 'homebridge' + +import { accessory } from './accessory' +import { device } from './device' +import { httpRequest, response } from './httpPromise' +import { domoticaPlatform } from './platform' + +export class powerSocketAccessory extends accessory { + private service: Service + + private state = { + On: false + } + + static async discoverDevices(platform: domoticaPlatform): Promise { + return new Promise((resolve, reject) => { + let powersocketDevices: device[] = [] + + httpRequest("http://" + platform.config.hostname + ':' + platform.config.port + '/API/inventory/powersocket') + .then((response) => { + let devices = JSON.parse(response.body) + + for (const device of devices) { + if (device.enabled == "true" && + device.switchable == "true") { + let object: device = { + type: "PowerSocket", + typeId: device.id, + uniqueId: "PowerSocket." + device.id, + deviceName: device.name, + switchable: device.switchable, + dimmable: "false", + detailedType: device.type + } + powersocketDevices.push(object) + } + } + + resolve(powersocketDevices) + }) + .catch((error) => { + reject('powerSocketAccessory::discoverDevices Error ->' + error) + }) + }) + } + + constructor( + private readonly platform: domoticaPlatform, + private readonly accessory: PlatformAccessory, + ) { + super("PowerSocket", accessory.context.device.typeId) + this.accessory.getService(this.platform.Service.AccessoryInformation)! + .setCharacteristic(this.platform.Characteristic.Manufacturer, 'PowerSocket') + .setCharacteristic(this.platform.Characteristic.Model, 'BW-SHP6') + .setCharacteristic(this.platform.Characteristic.SerialNumber, '02000' + accessory.context.device.typeId) + + if (accessory.context.device.detailedType == "light") { + this.service = this.accessory.getService(this.platform.Service.Lightbulb) || this.accessory.addService(this.platform.Service.Lightbulb) + } else { // powersocket + this.service = this.accessory.getService(this.platform.Service.Outlet) || this.accessory.addService(this.platform.Service.Outlet) + } + + if (accessory.context.device.switchable == "true") { + this.service.getCharacteristic(this.platform.Characteristic.On) + .onSet(this.setOn.bind(this)) + .onGet(this.getOn.bind(this)) + } else { + this.service.getCharacteristic(this.platform.Characteristic.On) + .onGet(this.getOn.bind(this)) + } + + this.setName(accessory.context.device.deviceName) + this.update() + } + + setName(name) { + this.service.setCharacteristic(this.platform.Characteristic.Name, name) + } + + setValue(key, value) { + if (key == "state" && value != "") { + this.state.On = (value == "1") + this.service.updateCharacteristic(this.platform.Characteristic.On, this.state.On) + } + } + + update() { + const data = JSON.stringify([{ + type: 'powersocket', + query: 'state' + }]) + + httpRequest("http://" + this.platform.config.hostname + ':' + this.platform.config.port + '/API/Status', data) + .then((response) => { + const devices = JSON.parse(response.body) + + for (const device of devices.powersocket) { + if (device.id == this.accessory.context.device.typeId) { + if (device.state == 'on') { + this.state.On = true + } else { + this.state.On = false + } + this.service.updateCharacteristic(this.platform.Characteristic.On, this.state.On) + break + } + } + }) + .catch((error) => { + this.platform.log.debug('powerSocketAccessory::update Error ->' + error) + }) + } + + async setOn(value: CharacteristicValue) { + this.state.On = value as boolean + + // http://$domoticaIP/external/WebUpdate.php?data=PowerSocket/$id/$state/ + httpRequest("http://" + this.platform.config.hostname + '/external/WebUpdate.php?data=PowerSocket/' + this.accessory.context.device.typeId + '/' + (this.state.On? '1': '0') + '/') + .catch((error) => { + this.platform.log.debug('powerSocketAccessory::setOn Error ->' + error) + }) + } + + getOn(): CharacteristicValue { + return this.state.On + } +} diff --git a/powerSwitchAccessory.ts b/powerSwitchAccessory.ts new file mode 100644 index 0000000..88ab331 --- /dev/null +++ b/powerSwitchAccessory.ts @@ -0,0 +1,152 @@ +import { Service, PlatformAccessory, CharacteristicValue } from 'homebridge' + +import { accessory } from './accessory' +import { device } from './device' +import { httpRequest, response } from './httpPromise' +import { domoticaPlatform } from './platform' + +export class powerSwitchAccessory extends accessory { + private service: Service + + private state = { + On: false, + Brightness: 100 + } + + static async discoverDevices(platform: domoticaPlatform): Promise { + return new Promise((resolve, reject) => { + let powerswitchDevices: device[] = [] + + httpRequest("http://" + platform.config.hostname + ':' + platform.config.port + '/API/inventory/powerswitch') + .then((response) => { + let devices = JSON.parse(response.body) + + for (const device of devices) { + if (device.enabled == "true") { + let object: device = { + type: "PowerSwitch", + typeId: device.id, + uniqueId: "PowerSwitch." + device.id, + deviceName: device.name, + switchable: "true", + dimmable: device.dimmable, + detailedType: device.type + } + powerswitchDevices.push(object) + } + } + + resolve(powerswitchDevices) + }) + .catch((error) => { + reject('powerSwitchAccessory::discoverDevices Error ->' + error) + }) + }) + } + + constructor( + private readonly platform: domoticaPlatform, + private readonly accessory: PlatformAccessory, + ) { + super("PowerSwitch", accessory.context.device.typeId) + this.accessory.getService(this.platform.Service.AccessoryInformation)! + .setCharacteristic(this.platform.Characteristic.Manufacturer, 'PowerSwitch') + .setCharacteristic(this.platform.Characteristic.Model, 'Shelly') + .setCharacteristic(this.platform.Characteristic.SerialNumber, '06000' + accessory.context.device.typeId) + + if (accessory.context.device.detailedType == "light") { + this.service = this.accessory.getService(this.platform.Service.Lightbulb) || this.accessory.addService(this.platform.Service.Lightbulb) + } else if (accessory.context.device.detailedType == "fan") { + this.service = this.accessory.getService(this.platform.Service.Fan) || this.accessory.addService(this.platform.Service.Fan) + } else { // other + this.service = this.accessory.getService(this.platform.Service.Outlet) || this.accessory.addService(this.platform.Service.Outlet) + } + + this.service.getCharacteristic(this.platform.Characteristic.On) + .onSet(this.setOn.bind(this)) + .onGet(this.getOn.bind(this)) + + if (accessory.context.device.dimmable != "false") { // WTF? + this.service.getCharacteristic(this.platform.Characteristic.Brightness) + .onSet(this.setBrightness.bind(this)) + .onGet(this.getBrightness.bind(this)) + } + + this.setName(accessory.context.device.deviceName) + this.update() + } + + setName(name) { + this.service.setCharacteristic(this.platform.Characteristic.Name, name) + } + + setValue(key, value) { + if (key == "state" && value != "") { + this.state.On = (value == "1") + this.service.updateCharacteristic(this.platform.Characteristic.On, this.state.On) + } else if (key == "dimLevel" && value != "") { + this.state.Brightness = value + this.service.updateCharacteristic(this.platform.Characteristic.Brightness, this.state.Brightness) + } + } + + update() { + const data = JSON.stringify([{ + type: 'powerswitch', + query: 'state' + }]) + + httpRequest("http://" + this.platform.config.hostname + ':' + this.platform.config.port + '/API/Status', data) + .then((response) => { + const devices = JSON.parse(response.body) + + for (const device of devices.powerswitch) { + if (device.id == this.accessory.context.device.typeId) { + if (device.state == 'on') { + this.state.On = true + } else { + this.state.On = false + } + this.service.updateCharacteristic(this.platform.Characteristic.On, this.state.On) + + if (device.dimmable != 'false') { // WTF? + this.state.Brightness = device.dimlevel; + this.service.updateCharacteristic(this.platform.Characteristic.Brightness, this.state.Brightness) + } + break + } + } + }) + .catch((error) => { + this.platform.log.debug('powerSwitchAccessory::update Error ->' + error) + }) + } + + async setOn(value: CharacteristicValue) { + this.state.On = value as boolean + + // http://$domoticaIP/external/WebUpdate.php?data=PowerSwitch/$id/$state/ + httpRequest("http://" + this.platform.config.hostname + '/external/WebUpdate.php?data=PowerSwitch/' + this.accessory.context.device.typeId + '/' + (this.state.On? '1': '0') + '/') + .catch((error) => { + this.platform.log.debug('powerSwitchAccessory::setOn Error ->' + error) + }) + } + + getOn(): CharacteristicValue { + return this.state.On + } + + async setBrightness(value: CharacteristicValue) { + this.state.Brightness = value as number + + // http://$domoticaIP/external/WebUpdate.php?data=PowerSwitch/$id/$state/$dimLevel + httpRequest("http://" + this.platform.config.hostname + '/external/WebUpdate.php?data=PowerSwitch/' + this.accessory.context.device.typeId + '//' + this.state.Brightness) + .catch((error) => { + this.platform.log.debug('powerSwitchAccessory::setBrightness Error ->' + error) + }) + } + + getBrightness(): CharacteristicValue { + return this.state.Brightness + } +} diff --git a/settings.ts b/settings.ts new file mode 100644 index 0000000..0990853 --- /dev/null +++ b/settings.ts @@ -0,0 +1,2 @@ +export const PLATFORM_NAME = 'Domotica' +export const PLUGIN_NAME = 'homebridge-domotica'