Skip to content

Commit

Permalink
add initial interval device update implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
regaw-leinad committed Mar 6, 2024
1 parent 9dc6942 commit 0889fc5
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 34 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ While not recommended, if manual setup is required, add the following to the `pl
"exposeSleepSwitch": false,
"filterReplacementIndicatorPercentage": 10,
"cacheIntervalSeconds": 300,
"updateIntervalSeconds": 300,
"deviceRefreshIntervalMinutes": 60,
"auth": {
"username": "your-email@domain.com",
Expand Down Expand Up @@ -116,6 +117,7 @@ While not recommended, if manual setup is required, add the following to the `pl
| `exposeSleepSwitch` | `false` | Whether to expose switches for Sleep mode on/off. |
| `filterReplacementIndicatorPercentage` | `10` | Percentage of filter life remaining to trigger a filter replacement alert. |
| `cacheIntervalSeconds` | `60` | Time, in seconds, for how long to reuse cached responses from Winix. |
| `updateIntervalSeconds` | `60` | Time, in seconds, for how often to poll Winix to update device state. |
| `deviceRefreshIntervalMinutes` | `60` | Time, in minutes, for how often to poll Winix to refresh the device list. |
| `auth.username` | `""` | Your Winix account username (email). This field is meant to be read-only after being generated in the UI. |
| `auth.userId` | `""` | Your Winix user ID for the Cognito User Pool. This field is meant to be read-only after being generated in the UI. |
Expand Down
14 changes: 14 additions & 0 deletions config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@
"maximum": 100,
"required": true
},
"doCacheWinix": {
"title": "Cache Winix Responses",
"description": "Cache Winix responses to reduce the number of requests to the Winix API",
"type": "boolean",
"default": true,
"required": true
},
"cacheIntervalSeconds": {
"title": "Winix Response Cache Interval (seconds)",
"description": "Time, in seconds, for how long to reuse cached responses from Winix",
Expand All @@ -54,6 +61,13 @@
"minimum": 0,
"required": true
},
"doUpdateIntervalDeviceState": {
"title": "Update Device State Interval",
"description": "Update the device state at a regular interval",
"type": "boolean",
"default": true,
"required": true
},
"deviceRefreshIntervalMinutes": {
"title": "Device Refresh Interval (minutes)",
"description": "Time, in minutes, for how often to poll Winix to refresh the device list. You can always just restart Homebridge to refresh the device list",
Expand Down
23 changes: 15 additions & 8 deletions src/accessory.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { CharacteristicValue, PlatformAccessory, Service } from 'homebridge';
import { Airflow, AirQuality, Mode, Plasmawave, Power } from 'winix-api';
import { CachedDevice, Device, UpdateIntervalDevice } from './device';
import { DeviceContext, WinixPurifierPlatform } from './platform';
import { DeviceOverride, WinixPlatformConfig } from './config';
import { CharacteristicManager } from './characteristic';
import { DeviceLogger } from './logger';
import { Device } from './device';

/**
* The maximum filter life in hours.
Expand All @@ -13,6 +13,7 @@ import { Device } from './device';
const MAX_FILTER_HOURS = 6480;
const DEFAULT_FILTER_LIFE_REPLACEMENT_PERCENTAGE = 10;
const DEFAULT_CACHE_INTERVAL_SECONDS = 60;
const DEFAULT_UPDATE_INTERVAL_SECONDS = 60;
const MIN_AMBIENT_LIGHT = 0.0001;

export class WinixPurifierAccessory {
Expand Down Expand Up @@ -40,8 +41,18 @@ export class WinixPurifierAccessory {
) {
const { deviceId, deviceAlias } = accessory.context.device;

const cacheIntervalMs = (config.cacheIntervalSeconds ?? DEFAULT_CACHE_INTERVAL_SECONDS) * 1000;
this.device = new Device(deviceId, cacheIntervalMs, this.log);
// We'll use a cached client if doCacheWinix is true, or if both doCacheWinix and doUpdateDeviceState are false
const cacheClient = config.doCacheWinix || !config.doUpdateDeviceState;

// Figure out if we're using a cached client or an interval-updating client
if (cacheClient) {
const cacheIntervalMs = (config.cacheIntervalSeconds ?? DEFAULT_CACHE_INTERVAL_SECONDS) * 1000;
this.device = new CachedDevice(deviceId, this.log, cacheIntervalMs);
} else {
const updateIntervalMs = (config.updateIntervalSeconds ?? DEFAULT_UPDATE_INTERVAL_SECONDS) * 1000;
this.device = new UpdateIntervalDevice(deviceId, this.log, updateIntervalMs);
}

this.servicesInUse = new Set<Service>();

const deviceSerial = override?.serialNumber ?? 'WNXAI00000000';
Expand Down Expand Up @@ -364,11 +375,7 @@ export class WinixPurifierAccessory {

private scheduleHomekitUpdate() {
this.log.debug('scheduling homekit update');

setTimeout(async () => {
await this.device.update();
await this.sendHomekitUpdate();
}, 1000);
setTimeout(async () => await this.device.update(), 1000);
}

/**
Expand Down
3 changes: 3 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ export interface WinixPlatformConfig {
exposeAutoSwitch?: boolean;
exposeSleepSwitch?: boolean;
filterReplacementIndicatorPercentage?: number;
doCacheWinix?: boolean;
cacheIntervalSeconds?: number;
doUpdateDeviceState?: boolean;
updateIntervalSeconds?: number;
deviceRefreshIntervalMinutes?: number;
deviceOverrides?: DeviceOverride[];
}
88 changes: 65 additions & 23 deletions src/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@ import AsyncLock from 'async-lock';
export interface DeviceState extends DeviceStatus {
}

export class Device {
/**
* Abstract class for interacting with a Winix device
*/
export abstract class Device {

private readonly lock: AsyncLock;
private state: DeviceState;
private lastWinixPoll = -1;
protected state: DeviceState;
protected lastWinixPoll = -1;

constructor(
private readonly deviceId: string,
private readonly cacheIntervalMs: number,
private readonly log: DeviceLogger,
protected constructor(
protected readonly deviceId: string,
protected readonly log: DeviceLogger,
) {
this.lock = new AsyncLock({ timeout: 3000 });
this.state = {
power: Power.Off,
mode: Mode.Auto,
Expand All @@ -28,6 +28,8 @@ export class Device {
};
}

protected abstract ensureUpdated(): Promise<void>;

hasData(): boolean {
return this.lastWinixPoll > -1;
}
Expand Down Expand Up @@ -132,24 +134,15 @@ export class Device {

async update(): Promise<void> {
this.log.debug('device:update()');
this.state = await WinixAPI.getDeviceStatus(this.deviceId);
const newState = await WinixAPI.getDeviceStatus(this.deviceId);
Object.assign(this.state, newState);
this.log.debug('device:update()', JSON.stringify(this.state));
this.lastWinixPoll = Date.now();
}

private async ensureUpdated(): Promise<void> {
// Use a lock to ensure only one update is running at a time
await this.lock.acquire('update', async () => {
if (this.shouldUpdate()) {
await this.update();
}
});
}

private shouldUpdate(): boolean {
return Date.now() - this.lastWinixPoll > this.cacheIntervalMs;
}

/**
* Ensures the device is on, returning true if it was turned on
*/
private async ensureOn(): Promise<boolean> {
if (await this.getPower() === Power.On) {
this.log.debug('device:ensureOn()', 'already on');
Expand All @@ -161,3 +154,52 @@ export class Device {
return true;
}
}

/**
* Implementation of Device that updates on a regular interval
*/
export class UpdateIntervalDevice extends Device {

constructor(
readonly deviceId: string,
readonly log: DeviceLogger,
readonly updateIntervalMs: number,
) {
super(deviceId, log);
setInterval(async () => await this.update(), updateIntervalMs);
}

protected async ensureUpdated(): Promise<void> {
// do nothing, since updating is handled by the interval
}
}

/**
* Implementation of Device that caches the state for a period of time
*/
export class CachedDevice extends Device {

private lock: AsyncLock;

constructor(
readonly deviceId: string,
readonly log: DeviceLogger,
private readonly cacheIntervalMs: number,
) {
super(deviceId, log);
this.lock = new AsyncLock({ timeout: 3000 });
}

protected async ensureUpdated(): Promise<void> {
// Use a lock to ensure only one update is running at a time
await this.lock.acquire('ensureUpdated', async () => {
if (this.shouldUpdate()) {
await this.update();
}
});
}

private shouldUpdate(): boolean {
return Date.now() - this.lastWinixPoll > this.cacheIntervalMs;
}
}
12 changes: 9 additions & 3 deletions src/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,17 @@ export class WinixPurifierPlatform implements DynamicPlatformPlugin {

async onFinishLaunching(): Promise<void> {
if (!this.config.auth?.refreshToken) {
this.log.warn('Winix Purifiers is NOT set up. ' +
this.log.error('Winix Purifiers is NOT set up. ' +
'Please link your Winix account in the Homebridge UI.');
return;
}

if (this.config.doCacheWinix && this.config.doUpdateDeviceState) {
this.log.error('doCacheWinix and doUpdateDeviceState cannot both be true. ' +
'Please set one or the other to true, or both to false in the Homebridge UI.');
return;
}

try {
this.winix = await WinixAccount.fromExistingAuth(this.config.auth);
} catch (e: unknown) {
Expand Down Expand Up @@ -83,13 +89,13 @@ export class WinixPurifierPlatform implements DynamicPlatformPlugin {

// if devices is explicitly typeof undefined, then the user has not logged in yet
if (typeof devices === 'undefined') {
this.log.warn('Winix Purifiers is NOT set up. ' +
this.log.error('Winix Purifiers is NOT set up. ' +
'Please log in with your Winix account credentials in the Homebridge UI.');
return;
}

if (devices.length === 0) {
this.log.warn('No Winix devices found. Please add devices to your Winix account.');
this.log.error('No Winix devices found. Please add devices to your Winix account.');
}

const accessoriesToAdd: PlatformAccessory<DeviceContext>[] = [];
Expand Down

0 comments on commit 0889fc5

Please sign in to comment.