Skip to content

Commit

Permalink
Fixes and Fakegato history support (#35)
Browse files Browse the repository at this point in the history
* Add fakegato

* Typo

* Fix fakegato config

* Fix fakegato config 2
Irrigation in use

* Init fakegato immediately

* Fix enhanced blind position update

* FakeGato fixes

* Only present power meter service

* Some refactoring about HAP types

* Add power meter aservice

* Don't use outlet

* Try to fix power meter service init

* Fix service definition

* Use HAP PowerManagement Service

* Use getCharacteristic

* Remove update

* No power meter service

* Add log

* Remove unused imports

* Use hap as dependency

* Expose new service

* Add get callback

* Another try

* Expose EVE characteristic through outlet service
  • Loading branch information
madchicken authored Mar 2, 2021
1 parent 0f6bdea commit 8e0e13f
Show file tree
Hide file tree
Showing 11 changed files with 716 additions and 208 deletions.
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@
"mqtt": "^4.2.6",
"mqtt-packet": "^6.7.0",
"prom-client": "^13.1.0",
"typescript": "^4.1.3"
"typescript": "^4.1.3",
"fakegato-history": "^0.6.1"
},
"devDependencies": {
"@types/express": "^4.17.2",
Expand All @@ -57,8 +58,8 @@
"eslint": "^6.4.0",
"eslint-config-prettier": "^6.10.0",
"eslint-plugin-prettier": "^3.1.2",
"hap-nodejs": "^0.7.7",
"homebridge": "^1.1.0",
"homebridge": "^1.3.1",
"hap-nodejs": "^0.9.2",
"husky": "^4.2.3",
"jest": "^24.9.0",
"nock": "^12.0.2",
Expand Down
147 changes: 14 additions & 133 deletions src/accessories/blind.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,21 @@
import { ComelitAccessory } from './comelit';
import { BlindDeviceData, ComelitClient, OBJECT_SUBTYPE, ObjectStatus } from 'comelit-client';
import { BlindDeviceData, ComelitClient, OBJECT_SUBTYPE } from 'comelit-client';
import { ComelitPlatform } from '../comelit-platform';
import { Callback, CharacteristicEventTypes, PlatformAccessory, Service } from 'homebridge';
import { CharacteristicEventTypes, PlatformAccessory, Service } from 'homebridge';
import { PositionState } from './hap';
import Timeout = NodeJS.Timeout;
import { CharacteristicSetCallback } from 'hap-nodejs';

export class Blind extends ComelitAccessory<BlindDeviceData> {
export abstract class Blind extends ComelitAccessory<BlindDeviceData> {
static readonly OPEN = 100;
static readonly CLOSED = 0;

static readonly OPENING_CLOSING_TIME = 35; // 35 seconds to open approx. We should have this in the config

private coveringService: Service;
private timeout: Timeout;
private lastCommandTime: number;
private readonly closingTime: number;
private positionState: number;
protected coveringService: Service;
protected positionState: number;

constructor(
platform: ComelitPlatform,
accessory: PlatformAccessory,
client: ComelitClient,
closingTime?: number
) {
constructor(platform: ComelitPlatform, accessory: PlatformAccessory, client: ComelitClient) {
super(platform, accessory, client);
this.closingTime = (closingTime || Blind.OPENING_CLOSING_TIME) * 1000;
this.log.info(`Blind ${accessory.context.id} has closing time of ${this.closingTime}`);
}

protected initServices(): Service[] {
Expand All @@ -43,126 +33,17 @@ export class Blind extends ComelitAccessory<BlindDeviceData> {

this.coveringService
.getCharacteristic(Characteristic.TargetPosition)
.on(CharacteristicEventTypes.SET, async (position: number, callback: Callback) => {
if (this.device.sub_type === OBJECT_SUBTYPE.ENHANCED_ELECTRIC_BLIND) {
await this.enhancedBlindSet(position, callback);
} else {
await this.standardBlindSet(position, callback);
.on(
CharacteristicEventTypes.SET,
async (position: number, callback: CharacteristicSetCallback) => {
await this.setPosition(position, callback);
}
});
);

return [accessoryInformation, this.coveringService];
}

private async enhancedBlindSet(position: number, callback: Callback) {
const Characteristic = this.platform.Characteristic;
try {
const currentPosition = this.coveringService.getCharacteristic(Characteristic.CurrentPosition)
.value as number;
this.log.info(`Setting position to ${position}%. Current position is ${currentPosition}`);
await this.client.setBlindPosition(this.device.id, Math.round(position * 2.55));
callback();
} catch (e) {
this.log.error(e.message);
callback(e);
}
}

private async standardBlindSet(position: number, callback: Callback) {
const Characteristic = this.platform.Characteristic;
try {
if (this.timeout) {
await this.resetTimeout();
callback();
return;
}

const currentPosition = this.coveringService.getCharacteristic(Characteristic.CurrentPosition)
.value as number;
const status = position < currentPosition ? ObjectStatus.OFF : ObjectStatus.ON;
const delta = currentPosition - position;
this.log.info(
`Setting position to ${position}%. Current position is ${currentPosition}. Delta is ${delta}`
);
if (delta !== 0) {
await this.client.toggleDeviceStatus(this.device.id, status);
this.lastCommandTime = new Date().getTime();
this.timeout = setTimeout(async () => {
return this.resetTimeout();
}, (this.closingTime * Math.abs(delta)) / 100);
}
callback();
} catch (e) {
this.log.error(e.message);
callback(e);
}
}
private async resetTimeout() {
// A timeout was set, this means that we are already opening or closing the blind
// Stop the blind and calculate a rough position
this.log.info(`Stopping blind`);
clearTimeout(this.timeout);
this.timeout = null;
await this.client.toggleDeviceStatus(
this.device.id,
this.positionState === PositionState.DECREASING ? ObjectStatus.ON : ObjectStatus.OFF
); // stop the blind
}

public update(data: BlindDeviceData) {
const Characteristic = this.platform.Characteristic;
const status = parseInt(data.status);
const now = new Date().getTime();
switch (status) {
case ObjectStatus.ON:
this.lastCommandTime = now;
this.positionState = PositionState.INCREASING;
break;
case ObjectStatus.OFF: {
const position = this.positionFromTime();
this.lastCommandTime = 0;
this.log.info(
`Blind is now at position ${position} (it was ${
this.positionState === PositionState.DECREASING ? 'going down' : 'going up'
})`
);
this.positionState = PositionState.STOPPED;
this.coveringService.getCharacteristic(Characteristic.TargetPosition).updateValue(position);
this.coveringService
.getCharacteristic(Characteristic.CurrentPosition)
.updateValue(position);
this.coveringService
.getCharacteristic(Characteristic.PositionState)
.updateValue(PositionState.STOPPED);
break;
}
case ObjectStatus.IDLE:
this.lastCommandTime = now;
this.positionState = PositionState.DECREASING;
break;
}
this.log.info(
`Blind update: status ${status}, state ${this.positionState}, ts ${this.lastCommandTime}`
);
}
public abstract setPosition(position: number, callback: CharacteristicSetCallback): Promise<void>;

private positionFromTime() {
const Characteristic = this.platform.Characteristic;
const now = new Date().getTime();
// Calculate the number of milliseconds the blind moved
const delta = now - this.lastCommandTime;
const currentPosition = this.coveringService.getCharacteristic(Characteristic.CurrentPosition)
.value as number;
// Calculate the percentage of movement
const deltaPercentage = Math.round(delta / (this.closingTime / 100));
this.log.info(
`Current position ${currentPosition}, delta is ${delta} (${deltaPercentage}%). State ${this.positionState}`
);
if (this.positionState === PositionState.DECREASING) {
// Blind is decreasing, subtract the delta
return currentPosition - deltaPercentage;
}
// Blind is increasing, add the delta
return currentPosition + deltaPercentage;
}
public abstract update(data: BlindDeviceData);
}
78 changes: 78 additions & 0 deletions src/accessories/enhanced-blind.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { BlindDeviceData, ComelitClient, ObjectStatus } from 'comelit-client';
import { ComelitPlatform } from '../comelit-platform';
import { Callback, PlatformAccessory } from 'homebridge';
import { PositionState } from './hap';
import { Blind } from './blind';

/**
* Returns the position as a value between 0 and 255
* @param position number 0-100
*/
function getPositionAsByte(position: number) {
return Math.round(position * 2.55);
}

/**
* Returns the position as a value between 0 and 100
* @param position number 0-255
*/
function getPositionAsPerc(position: string) {
return Math.round(parseInt(position) / 2.55);
}

export class EnhancedBlind extends Blind {
constructor(platform: ComelitPlatform, accessory: PlatformAccessory, client: ComelitClient) {
super(platform, accessory, client);
}

public async setPosition(position: number, callback: Callback) {
const Characteristic = this.platform.Characteristic;
try {
const currentPosition = this.coveringService.getCharacteristic(Characteristic.CurrentPosition)
.value as number;
this.log.info(`Setting position to ${position}%. Current position is ${currentPosition}`);
this.coveringService.setCharacteristic(
Characteristic.PositionState,
position > currentPosition ? PositionState.INCREASING : PositionState.DECREASING
);
await this.client.setBlindPosition(this.device.id, getPositionAsByte(position));
callback();
} catch (e) {
this.log.error(e.message);
callback(e);
}
}

public update(data: BlindDeviceData) {
const Characteristic = this.platform.Characteristic;
const position = getPositionAsPerc(data.position);
const status = parseInt(data.status); // can be 1 (increasing), 2 (decreasing) or 0 (stopped)
switch (status) {
case ObjectStatus.ON:
this.positionState = PositionState.INCREASING;
break;
case ObjectStatus.OFF: {
this.log.info(
`Blind is now at position ${position} (it was ${
this.positionState === PositionState.DECREASING ? 'closing' : 'opening'
})`
);
this.positionState = PositionState.STOPPED;
this.coveringService.getCharacteristic(Characteristic.TargetPosition).updateValue(position);
this.coveringService
.getCharacteristic(Characteristic.CurrentPosition)
.updateValue(position);
break;
}
case ObjectStatus.IDLE:
this.positionState = PositionState.DECREASING;
break;
}

this.coveringService
.getCharacteristic(Characteristic.PositionState)
.updateValue(this.positionState);
this.coveringService.getCharacteristic(Characteristic.TargetPosition).updateValue(position);
this.coveringService.getCharacteristic(Characteristic.CurrentPosition).updateValue(position);
}
}
3 changes: 0 additions & 3 deletions src/accessories/hap.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import { CharacteristicSetCallback } from 'hap-nodejs';
import { Logger } from 'homebridge';

export enum Active {
INACTIVE = 0,
ACTIVE = 1,
Expand Down
11 changes: 9 additions & 2 deletions src/accessories/irrigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ export class Irrigation extends ComelitAccessory<IrrigationDeviceData> {
public update(data: IrrigationDeviceData) {
const Characteristic = this.platform.Characteristic;
const status = parseInt(data.status);
this.service.updateCharacteristic(Characteristic.On, status);
this.service.updateCharacteristic(Characteristic.Active, status);
this.service.updateCharacteristic(Characteristic.InUse, status);
irrigationActivations.inc({ name: data.descrizione });
}

Expand All @@ -41,7 +42,7 @@ export class Irrigation extends ComelitAccessory<IrrigationDeviceData> {
this.accessory.addService(this.platform.Service.IrrigationSystem);
this.update(this.device);
this.service
.getCharacteristic(Characteristic.On)
.getCharacteristic(Characteristic.Active)
.on(CharacteristicEventTypes.SET, async (yes: boolean, callback: Function) => {
const status = yes ? Irrigation.ON : Irrigation.OFF;
try {
Expand All @@ -55,6 +56,12 @@ export class Irrigation extends ComelitAccessory<IrrigationDeviceData> {
.on(CharacteristicEventTypes.GET, (callback: CharacteristicGetCallback) => {
callback(null, this.device.status === `${ObjectStatus.ON}`);
});

this.service
.getCharacteristic(Characteristic.InUse)
.on(CharacteristicEventTypes.GET, (callback: CharacteristicGetCallback) => {
callback(null, this.device.status === `${ObjectStatus.ON}`);
});
return [accessoryInformation, this.service];
}
}
49 changes: 42 additions & 7 deletions src/accessories/power-supplier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,22 @@ import { ComelitAccessory } from './comelit';
import { ComelitClient, SupplierDeviceData } from 'comelit-client';
import client from 'prom-client';
import { ComelitPlatform } from '../comelit-platform';
import { PlatformAccessory, Service } from 'homebridge';
import {
CharacteristicEventTypes,
CharacteristicGetCallback,
PlatformAccessory,
Service,
} from 'homebridge';
import { HAP } from '../index';
import { FakegatoHistoryService } from '../types';

const consumption = new client.Gauge({
name: 'comelit_total_consumption',
help: 'Consumption in Wh',
});

export class PowerSupplier extends ComelitAccessory<SupplierDeviceData> {
private historyService: FakegatoHistoryService;
private outletService: Service;

constructor(platform: ComelitPlatform, accessory: PlatformAccessory, client: ComelitClient) {
Expand All @@ -21,20 +29,47 @@ export class PowerSupplier extends ComelitAccessory<SupplierDeviceData> {
this.accessory.getService(this.platform.Service.Outlet) ||
this.accessory.addService(this.platform.Service.Outlet);

if (!this.outletService.getCharacteristic(HAP.CurrentPowerConsumption)) {
this.outletService.addCharacteristic(HAP.CurrentPowerConsumption);
}

this.outletService
.getCharacteristic(this.platform.homebridge.hap.Characteristic.OutletInUse)
.on(CharacteristicEventTypes.GET, (callback: CharacteristicGetCallback) => {
callback(null, parseFloat(this.device.instant_power) > 0);
});
this.outletService
.getCharacteristic(this.platform.homebridge.hap.Characteristic.On)
.setValue(true);
.getCharacteristic(HAP.CurrentPowerConsumption)
.on(CharacteristicEventTypes.GET, (callback: CharacteristicGetCallback) => {
callback(null, parseFloat(this.device.instant_power));
});

this.historyService = new HAP.FakeGatoHistoryService('energy', this.accessory, {
log: this.log,
disableTimer: true,
storage: 'fs',
path: `${this.platform.homebridge.user.storagePath()}/accessories`,
filename: `history_${this.accessory.displayName}.json`,
});

return [this.initAccessoryInformation(), this.outletService];
return [this.initAccessoryInformation(), this.outletService, this.historyService];
}

update(data: SupplierDeviceData): void {
const instantPower = parseFloat(data.instant_power);
this.log.info(`Reporting instant consumption of ${instantPower}Wh`);
consumption.set(instantPower);

this.outletService
.getCharacteristic(this.platform.homebridge.hap.Characteristic.OutletInUse)
.updateValue(instantPower > 0);
this.outletService.updateCharacteristic(
this.platform.homebridge.hap.Characteristic.OutletInUse,
instantPower > 0
);

this.outletService.updateCharacteristic(HAP.CurrentPowerConsumption, instantPower);

this.historyService.addEntry({
time: Date.now() / 1000,
power: instantPower,
});
}
}
Loading

0 comments on commit 8e0e13f

Please sign in to comment.