Browse Source

Initial commit

JDierkse 3 năm trước cách đây
commit
e7947dc769

+ 13 - 0
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()
+}

+ 12 - 0
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
+}

+ 134 - 0
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<device[]> {
+		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
+	}
+}

+ 118 - 0
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<device[]> {
+		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
+	}
+}

+ 35 - 0
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<response> {
+	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()
+	})
+}

+ 98 - 0
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)
+	}
+}

+ 8 - 0
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)
+}

+ 147 - 0
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<device[]> {
+		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
+	}
+}

+ 111 - 0
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<device[]> {
+		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
+	}
+}

+ 189 - 0
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])
+			}
+		}
+	}
+}

+ 141 - 0
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<CharacteristicValue> {
+		// 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)
+	}
+
+}

+ 127 - 0
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<device[]> {
+		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
+	}
+}

+ 152 - 0
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<device[]> {
+		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
+	}
+}

+ 2 - 0
settings.ts

@@ -0,0 +1,2 @@
+export const PLATFORM_NAME = 'Domotica'
+export const PLUGIN_NAME = 'homebridge-domotica'