First v2 dev commit
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
AccessoryPlugin,
|
||||
CharacteristicValue,
|
||||
Service,
|
||||
Nullable,
|
||||
} from 'homebridge';
|
||||
|
||||
import { SmartHomeNGPlatform } from '../platform';
|
||||
|
||||
export class Fan implements AccessoryPlugin {
|
||||
private readonly deviceService: Service;
|
||||
private readonly informationService: Service;
|
||||
|
||||
public name: string;
|
||||
private active = false;
|
||||
|
||||
constructor(private readonly platform: SmartHomeNGPlatform, private readonly accessory) {
|
||||
this.name = accessory.name;
|
||||
this.deviceService = new this.platform.Service.Fanv2(accessory.name);
|
||||
|
||||
// create handlers for required characteristics
|
||||
this.deviceService.getCharacteristic(this.platform.Characteristic.Active)
|
||||
.onGet(this.getActive.bind(this))
|
||||
.onSet(this.setActive.bind(this));
|
||||
|
||||
this.informationService =
|
||||
new this.platform.Service.AccessoryInformation()
|
||||
.setCharacteristic(this.platform.Characteristic.Manufacturer, accessory.manufacturer)
|
||||
.setCharacteristic(this.platform.Characteristic.Model, accessory.model)
|
||||
.setCharacteristic(this.platform.Characteristic.SerialNumber, accessory.active);
|
||||
|
||||
this.platform.shng.addMonitor(accessory.active, this.shngCallback.bind(this));
|
||||
this.platform.log.info("Fan '%s' created!", accessory.name);
|
||||
}
|
||||
|
||||
identify(): void {
|
||||
this.platform.log.info('Identify!');
|
||||
}
|
||||
|
||||
getServices(): Service[] {
|
||||
return [this.informationService, this.deviceService];
|
||||
}
|
||||
|
||||
getActive(): Nullable<CharacteristicValue> {
|
||||
this.platform.log.info('getActive:', this.accessory.name, 'is currently', (this.active ? 'ON' : 'OFF'));
|
||||
return this.active;
|
||||
}
|
||||
|
||||
setActive(value: CharacteristicValue) {
|
||||
this.active = value as boolean;
|
||||
this.platform.log.info('setActive:', this.accessory.name, 'was set to', (this.active ? 'ON' : 'OFF'));
|
||||
this.platform.shng.setItem(this.accessory.active, this.active);
|
||||
}
|
||||
|
||||
shngCallback(value: unknown): void {
|
||||
this.platform.log.debug('shngCallback:', this.accessory.name, '=', value, '(' + typeof value + ')');
|
||||
if (typeof value === 'boolean') {
|
||||
this.active = value;
|
||||
this.deviceService.updateCharacteristic(this.platform.Characteristic.Active, this.active);
|
||||
} else {
|
||||
this.platform.log.warn('Unknown type ', typeof value, 'received for', this.accessory.name + ':', value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
import {
|
||||
AccessoryPlugin,
|
||||
CharacteristicValue,
|
||||
Service,
|
||||
Nullable,
|
||||
} from 'homebridge';
|
||||
|
||||
import { SmartHomeNGPlatform } from '../platform';
|
||||
|
||||
type RGBW = {
|
||||
R: number;
|
||||
G: number;
|
||||
B: number;
|
||||
W: number;
|
||||
};
|
||||
|
||||
type RGB = {
|
||||
R: number;
|
||||
G: number;
|
||||
B: number;
|
||||
};
|
||||
|
||||
export class Lightbulb implements AccessoryPlugin {
|
||||
private readonly deviceService: Service;
|
||||
private readonly informationService: Service;
|
||||
|
||||
public name: string;
|
||||
private switchOn = false;
|
||||
private brightness = 0; brightnessMin = 0; private brightnessMax = 1000;
|
||||
private R = 0; RMin = 0; RMax = 100;
|
||||
private G = 0; GMin = 0; GMax = 100;
|
||||
private B = 0; BMin = 0; BMax = 100;
|
||||
private W = 0; WMin = 0; WMax = 100;
|
||||
private hue = 0;
|
||||
private saturation = 0;
|
||||
private lightType = 0; // 0 = OnOff Light, 1 = Dimmable Light, 2 = RGB, 3 = RGBW
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
constructor(private readonly platform: SmartHomeNGPlatform, private readonly accessory: any) {
|
||||
this.name = accessory.name;
|
||||
this.deviceService = new this.platform.Service.Lightbulb(accessory.name);
|
||||
|
||||
// create handlers for required characteristics
|
||||
this.deviceService.getCharacteristic(this.platform.Characteristic.On)
|
||||
.onGet(this.getOn.bind(this))
|
||||
.onSet(this.setOn.bind(this));
|
||||
this.platform.shng.addMonitor(accessory.on, this.shngOnCallback.bind(this));
|
||||
|
||||
if (accessory.brightness) {
|
||||
this.deviceService.getCharacteristic(this.platform.Characteristic.Brightness)
|
||||
.onGet(this.getBrightness.bind(this))
|
||||
.onSet(this.setBrightness.bind(this));
|
||||
this.platform.shng.addMonitor(accessory.brightness, this.shngBrightnessCallback.bind(this));
|
||||
this.lightType = 1; // Dimmable light
|
||||
}
|
||||
|
||||
// If RGB or RGBW light
|
||||
if (accessory.r && accessory.g && accessory.b) {
|
||||
this.deviceService.getCharacteristic(this.platform.Characteristic.Hue)
|
||||
.onGet(this.getHue.bind(this))
|
||||
.onSet(this.setHue.bind(this));
|
||||
|
||||
this.deviceService.getCharacteristic(this.platform.Characteristic.Saturation)
|
||||
.onGet(this.getSaturation.bind(this))
|
||||
.onSet(this.setSaturation.bind(this));
|
||||
this.platform.shng.addMonitor(accessory.R, this.shngRCallback.bind(this));
|
||||
this.platform.shng.addMonitor(accessory.G, this.shngGCallback.bind(this));
|
||||
this.platform.shng.addMonitor(accessory.B, this.shngBCallback.bind(this));
|
||||
this.lightType = 2; // RGB light
|
||||
|
||||
if (accessory.w) {
|
||||
this.platform.shng.addMonitor(accessory.W, this.shngWCallback.bind(this));
|
||||
this.lightType = 3; // RGBW light
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
this.informationService =
|
||||
new this.platform.Service.AccessoryInformation()
|
||||
.setCharacteristic(this.platform.Characteristic.Manufacturer, accessory.manufacturer)
|
||||
.setCharacteristic(this.platform.Characteristic.Model, accessory.model)
|
||||
.setCharacteristic(this.platform.Characteristic.SerialNumber, accessory.on);
|
||||
|
||||
this.brightnessMax = accessory.brightnessmax ? accessory.brightnessmax : this.brightnessMax;
|
||||
this.brightnessMin = accessory.brightnessmin ? accessory.brightnessmin : this.brightnessMin;
|
||||
this.RMax = accessory.rmax ? accessory.rmax : this.RMax;
|
||||
this.RMin = accessory.rmin ? accessory.rmin : this.RMin;
|
||||
this.GMax = accessory.gmax ? accessory.gmax : this.GMax;
|
||||
this.GMin = accessory.gmin ? accessory.gmin : this.GMin;
|
||||
this.BMax = accessory.bmax ? accessory.bmax : this.BMax;
|
||||
this.BMin = accessory.bmin ? accessory.bmin : this.BMin;
|
||||
this.WMax = accessory.wmax ? accessory.wmax : this.WMax;
|
||||
this.WMin = accessory.wmin ? accessory.wmin : this.WMin;
|
||||
this.platform.log.info("Lightbulb '%s' created!", accessory.name);
|
||||
}
|
||||
|
||||
identify(): void {
|
||||
this.platform.log.info('Identify!');
|
||||
}
|
||||
|
||||
getServices(): Service[] {
|
||||
return [this.informationService, this.deviceService];
|
||||
}
|
||||
|
||||
getOn(): Nullable<CharacteristicValue> {
|
||||
this.platform.log.info('GetOn:', this.accessory.name, 'is currently', (this.switchOn ? 'ON' : 'OFF'));
|
||||
return this.switchOn;
|
||||
}
|
||||
|
||||
setOn(value: CharacteristicValue) {
|
||||
this.switchOn = value as boolean;
|
||||
this.platform.log.info('SetOn:', this.accessory.name, 'was set to', (this.switchOn ? 'ON' : 'OFF'));
|
||||
this.platform.shng.setItem(this.accessory.on, this.switchOn);
|
||||
}
|
||||
|
||||
getBrightness(): Nullable<CharacteristicValue> {
|
||||
this.platform.log.info('getBrightness:', this.accessory.name, 'brightness is currently', this.brightness);
|
||||
return this.brightness;
|
||||
}
|
||||
|
||||
setBrightness(value: CharacteristicValue) {
|
||||
this.brightness = value as number;
|
||||
this.platform.log.info('setBrightness:', this.accessory.name, 'was set to', value);
|
||||
|
||||
this.platform.shng.setItem(
|
||||
this.accessory.brightness,
|
||||
this.convertRange(this.brightness, 0, 100, this.brightnessMin, this.brightnessMax),
|
||||
);
|
||||
this.updateColor();
|
||||
}
|
||||
|
||||
getHue(): Nullable<CharacteristicValue> {
|
||||
this.platform.log.info('getHue:', this.accessory.name, 'hue is currently', this.brightness);
|
||||
return this.hue;
|
||||
}
|
||||
|
||||
setHue(value: CharacteristicValue) {
|
||||
this.hue = value as number;
|
||||
this.platform.log.info('setHue:', this.accessory.name, 'was set to', value);
|
||||
this.updateColor();
|
||||
}
|
||||
|
||||
getSaturation(): Nullable<CharacteristicValue> {
|
||||
this.platform.log.info('getSaturation:', this.accessory.name, 'saturation is currently', this.brightness);
|
||||
return this.saturation;
|
||||
}
|
||||
|
||||
setSaturation(value: CharacteristicValue) {
|
||||
this.saturation = value as number;
|
||||
this.platform.log.info('setSaturation:', this.accessory.name, 'was set to', value);
|
||||
this.updateColor();
|
||||
}
|
||||
|
||||
updateColor(): void {
|
||||
if (this.lightType === 3) {
|
||||
const rgbw: RGBW = this.hsb2rgbw(this.hue, this.saturation, this.brightness);
|
||||
this.platform.shng.setItem(this.accessory.r, this.convertRange(rgbw.R, 0, 100, this.RMin, this.RMax));
|
||||
this.platform.shng.setItem(this.accessory.g, this.convertRange(rgbw.G, 0, 100, this.GMin, this.GMax));
|
||||
this.platform.shng.setItem(this.accessory.b, this.convertRange(rgbw.B, 0, 100, this.BMin, this.BMax));
|
||||
this.platform.shng.setItem(this.accessory.w, this.convertRange(rgbw.W, 0, 100, this.WMin, this.WMax));
|
||||
} else if (this.lightType === 2) {
|
||||
const rgb: RGB = this.hsb2rgb(this.hue, this.saturation, this.brightness);
|
||||
this.platform.shng.setItem(this.accessory.r, this.convertRange(rgb.R, 0, 100, this.RMin, this.RMax));
|
||||
this.platform.shng.setItem(this.accessory.g, this.convertRange(rgb.G, 0, 100, this.GMin, this.GMax));
|
||||
this.platform.shng.setItem(this.accessory.b, this.convertRange(rgb.B, 0, 100, this.BMin, this.BMax));
|
||||
} else {
|
||||
this.platform.log.warn('Cannot update color of', this.name, 'because RGB(W) items are missing');
|
||||
}
|
||||
}
|
||||
|
||||
shngOnCallback(value: unknown): void {
|
||||
this.platform.log.debug('shngOnCallback:', this.accessory.name, '=', value, '(' + typeof value + ')');
|
||||
if (typeof value === 'boolean') {
|
||||
this.switchOn = value;
|
||||
this.deviceService.updateCharacteristic(this.platform.Characteristic.On, this.switchOn);
|
||||
} else {
|
||||
this.platform.log.warn('Unknown type ', typeof value, 'received for', this.accessory.name + ':', value);
|
||||
}
|
||||
}
|
||||
|
||||
shngBrightnessCallback(value: unknown): void {
|
||||
this.platform.log.debug('shngBrightnessCallback:', this.accessory.name, '=', value, '(' + typeof value + ')');
|
||||
if (typeof value === 'number') {
|
||||
this.brightness = this.convertRange(value as number, this.brightnessMin, this.brightnessMax, 0, 100);
|
||||
this.deviceService.updateCharacteristic(this.platform.Characteristic.Brightness, this.brightness);
|
||||
} else {
|
||||
this.platform.log.warn('Unknown type ', typeof value, 'received for', this.accessory.name + ':', value);
|
||||
}
|
||||
}
|
||||
|
||||
shngRCallback(value: unknown): void {
|
||||
// TODO
|
||||
}
|
||||
|
||||
shngGCallback(value: unknown): void {
|
||||
// TODO
|
||||
}
|
||||
|
||||
shngBCallback(value: unknown): void {
|
||||
// TODO
|
||||
}
|
||||
|
||||
shngWCallback(value: unknown): void {
|
||||
// TODO
|
||||
}
|
||||
|
||||
// Credits to: https://github.com/Sunoo/homebridge-gpio-rgbw-ledstrip/blob/master/src/index.ts
|
||||
hsb2rgbw(H: number, S: number, B: number): RGBW {
|
||||
const rgbw = { R: 0, G: 0, B: 0, W: 0 };
|
||||
|
||||
if (H === 0 && S === 0) {
|
||||
rgbw.W = B;
|
||||
} else {
|
||||
const segment = Math.floor(H / 60);
|
||||
const offset = H % 60;
|
||||
const mid = B * offset / 60;
|
||||
|
||||
rgbw.W = Math.round(B / 100 * (100 - S));
|
||||
|
||||
if (segment === 0) {
|
||||
rgbw.R = Math.round(S / 100 * B);
|
||||
rgbw.G = Math.round(S / 100 * mid);
|
||||
} else if (segment === 1) {
|
||||
rgbw.R = Math.round(S / 100 * (B - mid));
|
||||
rgbw.G = Math.round(S / 100 * B);
|
||||
} else if (segment === 2) {
|
||||
rgbw.G = Math.round(S / 100 * B);
|
||||
rgbw.B = Math.round(S / 100 * mid);
|
||||
} else if (segment === 3) {
|
||||
rgbw.G = Math.round(S / 100 * (B - mid));
|
||||
rgbw.B = Math.round(S / 100 * B);
|
||||
} else if (segment === 4) {
|
||||
rgbw.R = Math.round(S / 100 * mid);
|
||||
rgbw.B = Math.round(S / 100 * B);
|
||||
} else if (segment === 5) {
|
||||
rgbw.R = Math.round(S / 100 * B);
|
||||
rgbw.B = Math.round(S / 100 * (B - mid));
|
||||
}
|
||||
}
|
||||
|
||||
return rgbw;
|
||||
}
|
||||
|
||||
// Credits to: https://www.30secondsofcode.org/js/s/hsb-to-rgb
|
||||
hsb2rgb(H: number, S: number, B: number): RGB {
|
||||
const rgb = { R: 0, G: 0, B: 0 };
|
||||
S /= 100;
|
||||
B /= 100;
|
||||
const k = (n) => (n + H / 60) % 6;
|
||||
const f = (n) => B * (1 - S * Math.max(0, Math.min(k(n), 4 - k(n), 1)));
|
||||
rgb.R = f(5) + 100;
|
||||
rgb.G = f(3) + 100;
|
||||
rgb.B = f(1) + 100;
|
||||
return rgb;
|
||||
}
|
||||
|
||||
convertRange(value: number, oldmin: number, oldmax: number, newmin: number, newmax: number): number {
|
||||
return (((value - oldmin) * (newmax - newmin)) / (oldmax - oldmin)) + newmin;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
AccessoryPlugin,
|
||||
CharacteristicValue,
|
||||
Service,
|
||||
Nullable,
|
||||
} from 'homebridge';
|
||||
|
||||
import { SmartHomeNGPlatform } from '../platform';
|
||||
|
||||
export class MotionSensor implements AccessoryPlugin {
|
||||
private readonly deviceService: Service;
|
||||
private readonly informationService: Service;
|
||||
|
||||
public name: string;
|
||||
private motionDetected = false;
|
||||
|
||||
constructor(private readonly platform: SmartHomeNGPlatform, private readonly accessory) {
|
||||
this.name = accessory.name;
|
||||
this.deviceService = new this.platform.Service.MotionSensor(accessory.name);
|
||||
|
||||
// create handlers for required characteristics
|
||||
this.deviceService.getCharacteristic(this.platform.Characteristic.MotionDetected)
|
||||
.onGet(this.handleMotionDetectedGet.bind(this));
|
||||
|
||||
this.informationService =
|
||||
new this.platform.Service.AccessoryInformation()
|
||||
.setCharacteristic(this.platform.Characteristic.Manufacturer, accessory.manufacturer)
|
||||
.setCharacteristic(this.platform.Characteristic.Model, accessory.model)
|
||||
.setCharacteristic(this.platform.Characteristic.SerialNumber, accessory.motiondetected);
|
||||
|
||||
this.platform.shng.addMonitor(accessory.motiondetected, this.shngCallback.bind(this));
|
||||
this.platform.log.info('MotionSensor', accessory.name, 'created!');
|
||||
}
|
||||
|
||||
identify(): void {
|
||||
this.platform.log.info('Identify!');
|
||||
}
|
||||
|
||||
getServices(): Service[] {
|
||||
return [this.informationService, this.deviceService];
|
||||
}
|
||||
|
||||
handleMotionDetectedGet(): Nullable<CharacteristicValue> {
|
||||
this.platform.log.debug('handleMotionDetectedGet:', this.accessory.name, '=', this.motionDetected ? 'True' : 'False');
|
||||
return this.motionDetected;
|
||||
}
|
||||
|
||||
shngCallback(value: unknown): void {
|
||||
this.platform.log.debug('shngCallback:', this.accessory.name, '=', value, '(' + typeof value + ')');
|
||||
if (typeof value === 'boolean') {
|
||||
this.motionDetected = value;
|
||||
} else {
|
||||
this.platform.log.warn('Unknown type', typeof value, 'received for', this.accessory.name + ':', value);
|
||||
}
|
||||
this.deviceService.updateCharacteristic(this.platform.Characteristic.MotionDetected, this.motionDetected);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import {
|
||||
AccessoryPlugin,
|
||||
CharacteristicValue,
|
||||
Service,
|
||||
Nullable,
|
||||
} from 'homebridge';
|
||||
|
||||
import { SmartHomeNGPlatform } from '../platform';
|
||||
|
||||
export class OccupancySensor implements AccessoryPlugin {
|
||||
private readonly deviceService: Service;
|
||||
private readonly informationService: Service;
|
||||
|
||||
public name: string;
|
||||
private occupencyDetected = this.platform.Characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED;
|
||||
|
||||
constructor(private readonly platform: SmartHomeNGPlatform, private readonly accessory) {
|
||||
this.name = accessory.name;
|
||||
this.deviceService = new this.platform.Service.OccupancySensor(accessory.name);
|
||||
|
||||
// create handlers for required characteristics
|
||||
this.deviceService.getCharacteristic(this.platform.Characteristic.OccupancyDetected)
|
||||
.onGet(this.handleOccupancyDetectedGet.bind(this));
|
||||
|
||||
this.informationService =
|
||||
new this.platform.Service.AccessoryInformation()
|
||||
.setCharacteristic(this.platform.Characteristic.Manufacturer, accessory.manufacturer)
|
||||
.setCharacteristic(this.platform.Characteristic.Model, accessory.model)
|
||||
.setCharacteristic(this.platform.Characteristic.SerialNumber, accessory.occupancydetected);
|
||||
|
||||
this.platform.shng.addMonitor(accessory.occupancydetected, this.shngCallback.bind(this));
|
||||
this.platform.log.info('OccupancySensor', accessory.name, 'created!');
|
||||
}
|
||||
|
||||
identify(): void {
|
||||
this.platform.log.info('Identify!');
|
||||
}
|
||||
|
||||
getServices(): Service[] {
|
||||
return [this.informationService, this.deviceService];
|
||||
}
|
||||
|
||||
handleOccupancyDetectedGet(): Nullable<CharacteristicValue> {
|
||||
this.platform.log.debug('handleOccupancyDetectedGet:', this.accessory.name, '=', this.occupencyDetected ? 'True' : 'False');
|
||||
return this.occupencyDetected;
|
||||
}
|
||||
|
||||
shngCallback(value: unknown): void {
|
||||
this.platform.log.debug('shngCallback:', this.accessory.name, '=', value, '(' + typeof value + ')');
|
||||
if (typeof value === 'boolean') {
|
||||
if (value) {
|
||||
this.occupencyDetected = this.platform.Characteristic.OccupancyDetected.OCCUPANCY_DETECTED;
|
||||
} else {
|
||||
this.occupencyDetected = this.platform.Characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED;
|
||||
}
|
||||
} else {
|
||||
this.platform.log.warn('Unknown type', typeof value, 'received for', this.accessory.name + ':', value);
|
||||
}
|
||||
this.deviceService.updateCharacteristic(this.platform.Characteristic.OccupancyDetected, this.occupencyDetected);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
AccessoryPlugin,
|
||||
CharacteristicValue,
|
||||
Service,
|
||||
Nullable,
|
||||
} from 'homebridge';
|
||||
|
||||
import { SmartHomeNGPlatform } from '../platform';
|
||||
|
||||
export class Outlet implements AccessoryPlugin {
|
||||
private readonly deviceService: Service;
|
||||
private readonly informationService: Service;
|
||||
|
||||
public name: string;
|
||||
private switchOn = false;
|
||||
|
||||
constructor(private readonly platform: SmartHomeNGPlatform, private readonly accessory) {
|
||||
this.name = accessory.name;
|
||||
this.deviceService = new this.platform.Service.Outlet(accessory.name);
|
||||
|
||||
// create handlers for required characteristics
|
||||
this.deviceService.getCharacteristic(this.platform.Characteristic.On)
|
||||
.onGet(this.getOn.bind(this))
|
||||
.onSet(this.setOn.bind(this));
|
||||
|
||||
this.informationService =
|
||||
new this.platform.Service.AccessoryInformation()
|
||||
.setCharacteristic(this.platform.Characteristic.Manufacturer, accessory.manufacturer)
|
||||
.setCharacteristic(this.platform.Characteristic.Model, accessory.model)
|
||||
.setCharacteristic(this.platform.Characteristic.SerialNumber, accessory.on);
|
||||
|
||||
this.platform.shng.addMonitor(accessory.on, this.shngCallback.bind(this));
|
||||
this.platform.log.info("Outlet '%s' created!", accessory.name);
|
||||
}
|
||||
|
||||
identify(): void {
|
||||
this.platform.log.info('Identify!');
|
||||
}
|
||||
|
||||
getServices(): Service[] {
|
||||
return [this.informationService, this.deviceService];
|
||||
}
|
||||
|
||||
getOn(): Nullable<CharacteristicValue> {
|
||||
this.platform.log.info('GetOn:', this.accessory.name, 'is currently', (this.switchOn ? 'ON' : 'OFF'));
|
||||
return this.switchOn;
|
||||
}
|
||||
|
||||
setOn(value: CharacteristicValue) {
|
||||
this.switchOn = value as boolean;
|
||||
this.platform.log.info('SetOn:', this.accessory.name, 'was set to', (this.switchOn ? 'ON' : 'OFF'));
|
||||
this.platform.shng.setItem(this.accessory.on, this.switchOn);
|
||||
}
|
||||
|
||||
shngCallback(value: unknown): void {
|
||||
this.platform.log.debug('shngCallback:', this.accessory.name, '=', value, '(' + typeof value + ')');
|
||||
if (typeof value === 'boolean') {
|
||||
this.switchOn = value;
|
||||
this.deviceService.updateCharacteristic(this.platform.Characteristic.On, this.switchOn);
|
||||
} else {
|
||||
this.platform.log.warn('Unknown type ', typeof value, 'received for', this.accessory.name + ':', value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
AccessoryPlugin,
|
||||
CharacteristicValue,
|
||||
Service,
|
||||
Nullable,
|
||||
} from 'homebridge';
|
||||
|
||||
import { SmartHomeNGPlatform } from '../platform';
|
||||
|
||||
export class Switch implements AccessoryPlugin {
|
||||
private readonly deviceService: Service;
|
||||
private readonly informationService: Service;
|
||||
|
||||
public name: string;
|
||||
private switchOn = false;
|
||||
|
||||
constructor(private readonly platform: SmartHomeNGPlatform, private readonly accessory) {
|
||||
this.name = accessory.name;
|
||||
this.deviceService = new this.platform.Service.Switch(accessory.name);
|
||||
|
||||
// create handlers for required characteristics
|
||||
this.deviceService.getCharacteristic(this.platform.Characteristic.On)
|
||||
.onGet(this.getOn.bind(this))
|
||||
.onSet(this.setOn.bind(this));
|
||||
|
||||
this.informationService =
|
||||
new this.platform.Service.AccessoryInformation()
|
||||
.setCharacteristic(this.platform.Characteristic.Manufacturer, accessory.manufacturer)
|
||||
.setCharacteristic(this.platform.Characteristic.Model, accessory.model)
|
||||
.setCharacteristic(this.platform.Characteristic.SerialNumber, accessory.onoff);
|
||||
|
||||
this.platform.shng.addMonitor(accessory.onoff, this.shngCallback.bind(this));
|
||||
this.platform.log.info("Switch '%s' created!", accessory.name);
|
||||
}
|
||||
|
||||
identify(): void {
|
||||
this.platform.log.info('Identify!');
|
||||
}
|
||||
|
||||
getServices(): Service[] {
|
||||
return [this.informationService, this.deviceService];
|
||||
}
|
||||
|
||||
getOn(): Nullable<CharacteristicValue> {
|
||||
this.platform.log.info('GetOn:', this.accessory.name, 'is currently', (this.switchOn ? 'ON' : 'OFF'));
|
||||
return this.switchOn;
|
||||
}
|
||||
|
||||
setOn(value: CharacteristicValue) {
|
||||
this.switchOn = value as boolean;
|
||||
this.platform.log.info('SetOn:', this.accessory.name, 'was set to', (this.switchOn ? 'ON' : 'OFF'));
|
||||
this.platform.shng.setItem(this.accessory.onoff, this.switchOn);
|
||||
}
|
||||
|
||||
shngCallback(value: unknown): void {
|
||||
this.platform.log.debug('shngCallback:', this.accessory.name, '=', value, '(' + typeof value + ')');
|
||||
if (typeof value === 'boolean') {
|
||||
this.switchOn = value;
|
||||
this.deviceService.updateCharacteristic(this.platform.Characteristic.On, this.switchOn);
|
||||
} else {
|
||||
this.platform.log.warn('Unknown type ', typeof value, 'received for', this.accessory.name + ':', value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
AccessoryPlugin,
|
||||
CharacteristicValue,
|
||||
Service,
|
||||
Nullable,
|
||||
} from 'homebridge';
|
||||
|
||||
import { SmartHomeNGPlatform } from '../platform';
|
||||
|
||||
export class TemperatureSensor implements AccessoryPlugin {
|
||||
private readonly deviceService: Service;
|
||||
private readonly informationService: Service;
|
||||
|
||||
public name: string;
|
||||
private currentTemperature = 0;
|
||||
|
||||
constructor(private readonly platform: SmartHomeNGPlatform, private readonly accessory) {
|
||||
this.name = accessory.name;
|
||||
this.deviceService = new this.platform.Service.TemperatureSensor(accessory.name);
|
||||
|
||||
// create handlers for required characteristics
|
||||
this.deviceService.getCharacteristic(this.platform.Characteristic.CurrentTemperature)
|
||||
.onGet(this.getCurrentTemperature.bind(this));
|
||||
|
||||
this.informationService =
|
||||
new this.platform.Service.AccessoryInformation()
|
||||
.setCharacteristic(this.platform.Characteristic.Manufacturer, accessory.manufacturer)
|
||||
.setCharacteristic(this.platform.Characteristic.Model, accessory.model)
|
||||
.setCharacteristic(this.platform.Characteristic.SerialNumber, accessory.currenttemperature);
|
||||
|
||||
this.platform.shng.addMonitor(accessory.currenttemperature, this.shngCallback.bind(this));
|
||||
this.platform.log.info('TemperatureSensor', accessory.name, 'created!');
|
||||
}
|
||||
|
||||
identify(): void {
|
||||
this.platform.log.info('Identify!');
|
||||
}
|
||||
|
||||
getServices(): Service[] {
|
||||
return [this.informationService, this.deviceService];
|
||||
}
|
||||
|
||||
getCurrentTemperature(): Nullable<CharacteristicValue> {
|
||||
this.platform.log.debug('getCurrentTemperature:', this.accessory.name, '=', this.currentTemperature);
|
||||
return this.currentTemperature;
|
||||
}
|
||||
|
||||
shngCallback(value: unknown): void {
|
||||
this.platform.log.debug('shngCallback:', this.accessory.name, '=', value, '(' + typeof value + ')');
|
||||
if (typeof value === 'number') {
|
||||
this.currentTemperature = value;
|
||||
} else {
|
||||
this.platform.log.warn('Unknown type', typeof value, 'received for', this.accessory.name + ':', value);
|
||||
}
|
||||
this.deviceService.updateCharacteristic(this.platform.Characteristic.CurrentTemperature, this.currentTemperature);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import {
|
||||
AccessoryPlugin,
|
||||
CharacteristicValue,
|
||||
Service,
|
||||
Nullable,
|
||||
} from 'homebridge';
|
||||
|
||||
import { SmartHomeNGPlatform } from '../platform';
|
||||
|
||||
export class Thermostat implements AccessoryPlugin {
|
||||
private readonly deviceService: Service;
|
||||
private readonly informationService: Service;
|
||||
|
||||
public name: string;
|
||||
private currentTemperature = 0;
|
||||
private targetTemperature = 0;
|
||||
private targetTemperatureDisplayUnit = this.platform.Characteristic.TemperatureDisplayUnits.CELSIUS;
|
||||
private currentHeatingCoolingState = this.platform.Characteristic.CurrentHeatingCoolingState.OFF;
|
||||
private targetHeatingCoolingState = this.platform.Characteristic.TargetHeatingCoolingState.AUTO;
|
||||
|
||||
constructor(private readonly platform: SmartHomeNGPlatform, private readonly accessory) {
|
||||
this.name = accessory.name;
|
||||
this.deviceService = new this.platform.Service.Thermostat(accessory.name);
|
||||
|
||||
// create handlers for required characteristics
|
||||
this.deviceService.getCharacteristic(this.platform.Characteristic.CurrentTemperature)
|
||||
.onGet(this.getCurrentTemperature.bind(this));
|
||||
|
||||
this.deviceService.getCharacteristic(this.platform.Characteristic.TargetTemperature)
|
||||
.onGet(this.getTargetTemperature.bind(this))
|
||||
.onSet(this.setTargetTemperature.bind(this));
|
||||
|
||||
this.deviceService.getCharacteristic(this.platform.Characteristic.TemperatureDisplayUnits)
|
||||
.onGet(this.getTemperatureDisplayUnits.bind(this))
|
||||
.onSet(this.setTemperatureDisplayUnits.bind(this));
|
||||
|
||||
this.deviceService.getCharacteristic(this.platform.Characteristic.CurrentHeatingCoolingState)
|
||||
.onGet(this.getCurrentHeatingCoolingState.bind(this));
|
||||
|
||||
this.deviceService.getCharacteristic(this.platform.Characteristic.TargetHeatingCoolingState)
|
||||
.onGet(this.getTargetHeatingCoolingState.bind(this))
|
||||
.onSet(this.setTargetHeatingCoolingState.bind(this));
|
||||
|
||||
this.informationService =
|
||||
new this.platform.Service.AccessoryInformation()
|
||||
.setCharacteristic(this.platform.Characteristic.Manufacturer, accessory.manufacturer)
|
||||
.setCharacteristic(this.platform.Characteristic.Model, accessory.model)
|
||||
.setCharacteristic(this.platform.Characteristic.SerialNumber, accessory.currenttemperature);
|
||||
|
||||
// Add items to SHNG WS monitor
|
||||
this.platform.shng.addMonitor(accessory.currenttemperature, this.shngCurrentTemperatureCallback.bind(this));
|
||||
this.platform.shng.addMonitor(accessory.targettemperature, this.shngTargetTemperatureCallback.bind(this));
|
||||
this.platform.shng.addMonitor(accessory.currentheatingcoolingstate, this.shngCurrentHeatingCoolingStateCallback.bind(this));
|
||||
// FUTURE: Add monitoring of target Heating / Cooling states amd maybe display units
|
||||
this.platform.log.info('Thermostat', accessory.name, 'created!');
|
||||
}
|
||||
|
||||
identify(): void {
|
||||
this.platform.log.info('Identify!');
|
||||
}
|
||||
|
||||
getServices(): Service[] {
|
||||
return [this.informationService, this.deviceService];
|
||||
}
|
||||
|
||||
getCurrentTemperature(): Nullable<CharacteristicValue> {
|
||||
this.platform.log.debug('getCurrentTemperature:', this.accessory.name, '=', this.currentTemperature);
|
||||
return this.currentTemperature;
|
||||
}
|
||||
|
||||
getTargetTemperature(): Nullable<CharacteristicValue> {
|
||||
this.platform.log.debug('getTargetTemperature:', this.accessory.name, '=', this.targetTemperature);
|
||||
return this.targetTemperature;
|
||||
}
|
||||
|
||||
setTargetTemperature(value: CharacteristicValue) {
|
||||
this.platform.log.debug('setTargetTemperature:', this.accessory.name, 'to', value);
|
||||
this.targetTemperature = value as number;
|
||||
this.platform.shng.setItem(this.accessory.targettemperature, this.targetTemperature);
|
||||
}
|
||||
|
||||
getTemperatureDisplayUnits(): Nullable<CharacteristicValue> {
|
||||
this.platform.log.debug('getTemperatureDisplayUnits:', this.accessory.name, '=', this.targetTemperatureDisplayUnit);
|
||||
return this.targetTemperatureDisplayUnit;
|
||||
}
|
||||
|
||||
setTemperatureDisplayUnits(value: CharacteristicValue) {
|
||||
this.platform.log.debug('setTemperatureDisplayUnits:', this.accessory.name, 'to', value);
|
||||
this.targetTemperatureDisplayUnit = value as number;
|
||||
// FUTURE: Bind this to SHNG if needed
|
||||
}
|
||||
|
||||
getCurrentHeatingCoolingState(): Nullable<CharacteristicValue> {
|
||||
this.platform.log.debug('getCurrentHeatingCoolingState:', this.accessory.name, '=', this.currentHeatingCoolingState);
|
||||
return this.currentHeatingCoolingState;
|
||||
}
|
||||
|
||||
getTargetHeatingCoolingState(): Nullable<CharacteristicValue> {
|
||||
this.platform.log.debug('getTargetHeatingCoolingState:', this.accessory.name, '=', this.targetHeatingCoolingState);
|
||||
return this.targetHeatingCoolingState;
|
||||
}
|
||||
|
||||
setTargetHeatingCoolingState(value: CharacteristicValue) {
|
||||
this.platform.log.debug('setTargetHeatingCoolingState:', this.accessory.name, 'to', value);
|
||||
this.targetHeatingCoolingState = value as number;
|
||||
// FUTURE: Bind this to SHNG if needed
|
||||
}
|
||||
|
||||
shngCurrentTemperatureCallback(value: unknown): void {
|
||||
this.platform.log.debug('shngCurrentTemperatureCallback:', this.accessory.name, '=', value, '(' + typeof value + ')');
|
||||
if (typeof value === 'number') {
|
||||
this.currentTemperature = value;
|
||||
} else {
|
||||
this.platform.log.warn('Unknown type', typeof value, 'received for', this.accessory.name + ':', value);
|
||||
}
|
||||
this.deviceService.updateCharacteristic(this.platform.Characteristic.CurrentTemperature, this.currentTemperature);
|
||||
}
|
||||
|
||||
shngTargetTemperatureCallback(value: unknown): void {
|
||||
this.platform.log.debug('shngTargetTemperatureCallback:', this.accessory.name, '=', value, '(' + typeof value + ')');
|
||||
if (typeof value === 'number') {
|
||||
this.targetTemperature = value;
|
||||
} else {
|
||||
this.platform.log.warn('Unknown type', typeof value, 'received for', this.accessory.name + ':', value);
|
||||
}
|
||||
this.deviceService.updateCharacteristic(this.platform.Characteristic.TargetTemperature, this.targetTemperature);
|
||||
}
|
||||
|
||||
shngCurrentHeatingCoolingStateCallback(value: unknown): void {
|
||||
this.platform.log.debug('shngCurrentHeatingCoolingStateCallback:', this.accessory.name, '=', value, '(' + typeof value + ')');
|
||||
if (typeof value === 'number') {
|
||||
switch (value) {
|
||||
case 1:
|
||||
this.currentHeatingCoolingState = this.platform.Characteristic.CurrentHeatingCoolingState.HEAT;
|
||||
break;
|
||||
case 2:
|
||||
this.currentHeatingCoolingState = this.platform.Characteristic.CurrentHeatingCoolingState.COOL;
|
||||
break;
|
||||
default:
|
||||
this.currentHeatingCoolingState = this.platform.Characteristic.CurrentHeatingCoolingState.OFF;
|
||||
break;
|
||||
}
|
||||
this.deviceService.updateCharacteristic(
|
||||
this.platform.Characteristic.CurrentHeatingCoolingState,
|
||||
this.currentHeatingCoolingState,
|
||||
);
|
||||
} else {
|
||||
this.platform.log.warn('Unknown type', typeof value, 'received for', this.accessory.name + ':', value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import WebSocket from 'ws';
|
||||
import { SmartHomeNGPlatform } from './platform';
|
||||
|
||||
|
||||
export class SmartHomeNG {
|
||||
private ws: WebSocket;
|
||||
private tomonitor = {};
|
||||
|
||||
public connected = false;
|
||||
|
||||
constructor(private readonly platform: SmartHomeNGPlatform, private url: string, private autoReconnectInterval = 10) {
|
||||
this.platform.log.debug('SHNG constructor');
|
||||
}
|
||||
|
||||
connect() {
|
||||
this.ws = new WebSocket(this.url);
|
||||
this.ws.on('open', () => {
|
||||
this.platform.log.info('Connected to SmartHomeNG on', this.url);
|
||||
this.connected = true;
|
||||
this.identifyMyself();
|
||||
this.startMonitoring();
|
||||
});
|
||||
|
||||
this.ws.on('message', (data: string) => {
|
||||
this.receive(data);
|
||||
});
|
||||
|
||||
this.ws.on('error', (error) => {
|
||||
this.platform.log.error('WebSocket error: ' + error.toString());
|
||||
});
|
||||
|
||||
this.ws.on('close', (code: number) => {
|
||||
this.platform.log.warn('Lost connection (Code: ' + code + '). Reconnecting in', this.autoReconnectInterval, 'seconds.');
|
||||
this.connected = false;
|
||||
if (this.autoReconnectInterval > 0) {
|
||||
this.reconnect();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
reconnect() {
|
||||
this.ws.removeAllListeners();
|
||||
setTimeout(() => {
|
||||
this.platform.log.info('WebSocketClient: reconnecting...');
|
||||
this.connect();
|
||||
}, this.autoReconnectInterval * 1000);
|
||||
}
|
||||
|
||||
identifyMyself(): void {
|
||||
const protocol = { 'cmd': 'proto', 'ver': 4 };
|
||||
const identify = { 'cmd': 'identity', 'sw': 'homebridge-smarthomeng', 'ver': '2.0' };
|
||||
|
||||
this.send(protocol);
|
||||
this.send(identify);
|
||||
}
|
||||
|
||||
addMonitor(item: string, callback: (value: unknown) => void): void {
|
||||
if (item !== null && item !== 'undefined') {
|
||||
this.tomonitor[item] = callback;
|
||||
}
|
||||
}
|
||||
|
||||
startMonitoring(): void {
|
||||
const itemlist: string[] = [];
|
||||
for (const item in this.tomonitor) {
|
||||
itemlist.push(item);
|
||||
}
|
||||
if (itemlist.length > 0) {
|
||||
this.platform.log.info('Start monitoring ' + itemlist);
|
||||
const buffer = {
|
||||
'cmd': 'monitor',
|
||||
'items': itemlist,
|
||||
};
|
||||
this.send(buffer);
|
||||
} else {
|
||||
this.platform.log.warn('No need to start monitoring because no items are defined !');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
setItem(item: string, value: unknown): void {
|
||||
const command = { 'cmd': 'item', 'id': item, 'val': value };
|
||||
this.send(command);
|
||||
}
|
||||
|
||||
send(buffer: unknown): boolean {
|
||||
const command = JSON.stringify(buffer);
|
||||
if (this.connected) {
|
||||
this.platform.log.debug('WS: sending:', command);
|
||||
this.ws.send(command);
|
||||
return true;
|
||||
} else {
|
||||
this.platform.log.warn('Error sending command !');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
receive(data: string): void {
|
||||
const msg = JSON.parse(data);
|
||||
this.platform.log.debug('WS: received: %s', data);
|
||||
|
||||
if (msg.cmd === 'item' && msg.items) {
|
||||
for (const item of msg.items) {
|
||||
const name = item[0];
|
||||
const value = item[1];
|
||||
if (name in this.tomonitor) {
|
||||
const callback = this.tomonitor[name];
|
||||
this.platform.log.info('Received value', value, 'for', name, 'callback', callback);
|
||||
callback(value);
|
||||
} else {
|
||||
this.platform.log.debug('Ignoring unmonitored item', name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { API } from 'homebridge';
|
||||
|
||||
import { PLATFORM_NAME } from './settings';
|
||||
import { SmartHomeNGPlatform } from './platform';
|
||||
|
||||
/**
|
||||
* This method registers the platform with Homebridge
|
||||
*/
|
||||
export = (api: API) => {
|
||||
api.registerPlatform(PLATFORM_NAME, SmartHomeNGPlatform);
|
||||
};
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
import {
|
||||
API,
|
||||
StaticPlatformPlugin,
|
||||
Logger,
|
||||
PlatformConfig,
|
||||
AccessoryPlugin,
|
||||
Service,
|
||||
Characteristic,
|
||||
} from 'homebridge';
|
||||
import { SmartHomeNG } from './SmartHomeNG';
|
||||
|
||||
import { OccupancySensor } from './Accessories/OccupancySensor';
|
||||
import { MotionSensor } from './Accessories/MotionSensor';
|
||||
import { Switch } from './Accessories/Switch';
|
||||
import { Outlet } from './Accessories/Outlet';
|
||||
import { Fan } from './Accessories/Fan';
|
||||
import { Lightbulb } from './Accessories/Lightbulb';
|
||||
import { TemperatureSensor } from './Accessories/TemperatureSensor';
|
||||
import { Thermostat } from './Accessories/Thermostat';
|
||||
|
||||
const uncapitalizeKeys = (obj) => {
|
||||
const isObject = o => Object.prototype.toString.apply(o) === '[object Object]';
|
||||
const isArray = o => Object.prototype.toString.apply(o) === '[object Array]';
|
||||
|
||||
const transformedObj = isArray(obj) ? [] : {};
|
||||
|
||||
for (const key in obj) {
|
||||
const transformedKey = key.toLowerCase();
|
||||
|
||||
if (isObject(obj[key]) || isArray(obj[key])) {
|
||||
transformedObj[transformedKey] = uncapitalizeKeys(obj[key]);
|
||||
} else {
|
||||
transformedObj[transformedKey] = obj[key];
|
||||
}
|
||||
}
|
||||
return transformedObj;
|
||||
};
|
||||
|
||||
export class SmartHomeNGPlatform implements StaticPlatformPlugin {
|
||||
public readonly Service: typeof Service = this.api.hap.Service;
|
||||
public readonly Characteristic: typeof Characteristic = this.api.hap.Characteristic;
|
||||
public shng: SmartHomeNG;
|
||||
|
||||
constructor(
|
||||
public readonly log: Logger,
|
||||
public readonly config: PlatformConfig,
|
||||
public readonly api: API,
|
||||
) {
|
||||
log.debug('Using config file', api.user.configPath());
|
||||
this.shng = new SmartHomeNG(this, 'ws://smarthome.iot.wagener.family:2424/');
|
||||
|
||||
this.api.on('didFinishLaunching', () => {
|
||||
log.debug('Executed didFinishLaunching callback');
|
||||
this.shng.connect();
|
||||
});
|
||||
}
|
||||
|
||||
accessories(callback: (foundAccessories: AccessoryPlugin[]) => void): void {
|
||||
this.log.debug('Building accessories list...');
|
||||
const devices: AccessoryPlugin[] = [];
|
||||
|
||||
// convert all configured accessory keys to lowercase to tolerate user case errors
|
||||
const accessories = JSON.parse(JSON.stringify(uncapitalizeKeys(this.config.accessories)));
|
||||
// this.log.debug(accessories);
|
||||
|
||||
for (const accessory of accessories) {
|
||||
//this.log.debug(accessory);
|
||||
if (!accessory.manufacturer) {
|
||||
accessory.manufacturer = 'SmartHomeNG';
|
||||
}
|
||||
if (!accessory.model) {
|
||||
accessory.model = 'SHNG Item';
|
||||
}
|
||||
|
||||
if (accessory.type !== '') {
|
||||
this.log.info('Parsing accessory:', accessory.name, 'of type', accessory.type);
|
||||
// set lo lowercase to ignore user case errors
|
||||
switch (accessory.type.toLowerCase()) {
|
||||
|
||||
// Presence sensor
|
||||
case 'occupancysensor':
|
||||
devices.push(new OccupancySensor(this, accessory));
|
||||
break;
|
||||
// Motion sensor
|
||||
case 'motionsensor':
|
||||
devices.push(new MotionSensor(this, accessory));
|
||||
break;
|
||||
|
||||
// Switch
|
||||
case 'switch':
|
||||
devices.push(new Switch(this, accessory));
|
||||
break;
|
||||
|
||||
// Lightbulb
|
||||
case 'lightbulb':
|
||||
devices.push(new Lightbulb(this, accessory));
|
||||
break;
|
||||
|
||||
// Outlet
|
||||
case 'outlet':
|
||||
devices.push(new Outlet(this, accessory));
|
||||
break;
|
||||
|
||||
// Fan (uses Fanv2)
|
||||
case 'fan':
|
||||
devices.push(new Fan(this, accessory));
|
||||
break;
|
||||
|
||||
// TemperatureSensor
|
||||
case 'temperaturesensor':
|
||||
devices.push(new TemperatureSensor(this, accessory));
|
||||
break;
|
||||
|
||||
// TemperatureSensor
|
||||
case 'thermostat':
|
||||
devices.push(new Thermostat(this, accessory));
|
||||
break;
|
||||
|
||||
default:
|
||||
this.log.warn('Accessory type', accessory.type, 'for', accessory.name, 'not recognized !');
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
this.log.warn('Ignoring accessory (no type given): ' + accessory);
|
||||
}
|
||||
}
|
||||
callback(devices);
|
||||
this.log.debug('Finished building accessories list');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* This is the name of the platform that users will use to register the plugin in the Homebridge config.json
|
||||
*/
|
||||
export const PLATFORM_NAME = 'SmartHomeNG';
|
||||
|
||||
/**
|
||||
* This must match the name of your plugin as defined the package.json
|
||||
*/
|
||||
export const PLUGIN_NAME = 'homebridge-smarthomeng';
|
||||
Reference in New Issue
Block a user