commit e7947dc769dabdf55478dde40709cff2e3f02221 Author: JDierkse Date: Tue Jul 12 16:34:41 2022 +0200 Initial commit 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'