Skip to content

Commit

Permalink
Release 1.1.1: Pushes status updates to HomeKit API as oven status ch…
Browse files Browse the repository at this point in the history
…anges, fixes null dereference on wet bulb temperature status
  • Loading branch information
jon-bell committed Jul 25, 2023
1 parent b9ad9d0 commit b7bc396
Show file tree
Hide file tree
Showing 6 changed files with 67 additions and 13 deletions.
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@ Default recipes:
This plugin was created based on the code in [create new repository from template](https://github.com/homebridge/homebridge-plugin-template/generate)

## Release notes
- Release 1.1.1: Pushes status updates to HomeKit API as oven status changes, fixes null dereference on wet bulb temperature status
- Release 1.1.0: Support for Anova Oven Protocol Version 2
- Release 1.0.0: Initial release (supports Anova Oven Protocol Version 1 only)

## Local dev
`npm run build && npm link && DEBUG=* homebridge -D -C -U .` (uses `config.json` in cwd)

## Protocol notes
Inspiration from [mcolyer's oven API V1 reverse engineering results](https://mcolyer.github.io/anova-oven-api/).

Expand All @@ -44,7 +48,8 @@ Android apps can specify whether or not to trust user-installed root certificate
1. Download the most recent Anova Oven App APK (e.g. [from apkpure](https://m.apkpure.com/anova-oven/com.anovaculinary.anovaoven))
2. Use [apktool](https://apktool.org) to unwrap the apk
3. Edit the file `res/xml/network_security_config.xml`:
* Before:

Before:
```
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
Expand All @@ -55,7 +60,8 @@ Android apps can specify whether or not to trust user-installed root certificate
</domain-config>
</network-security-config>
```
* After:
After:

```
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"displayName": "Homebridge Anova Oven Plugin",
"name": "homebridge-plugin-anova-toast",
"version": "1.1.0",
"version": "1.1.1",
"description": "A plugin that allows you to control your Anova Precision Oven from Homebridge. Right now, it just lets you make toast.",
"license": "Apache-2.0",
"repository": {
Expand Down
32 changes: 28 additions & 4 deletions src/AnovaOvenService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import TypedEventEmitter from 'typed-emitter';
import { v4 as uuidv4 } from 'uuid';
import WebSocket from 'ws';
import {
AnovaOvenResponse, DeviceID, OvenCommand, OvenCommandResponse, OvenResponse, OvenStateMessage,
AnovaOvenEvent, DeviceID, OvenCommand, OvenCommandResponse, OvenResponse, OvenStateMessage,
StagesEntity, StartCookPayload, SteamGenerators, TemperatureBulbSetPoint,
} from './AnovaTypes';
import { Logger } from 'homebridge';
Expand All @@ -21,6 +21,8 @@ const steamGeneratorsEqual = (a: SteamGenerators | undefined, b: SteamGenerators
}
if (a.mode === 'steam-percentage') {
return a.steamPercentage?.setpoint === b.steamPercentage?.setpoint;
} else if(a.mode === 'relative-humidity'){
return a.relativeHumidity?.setpoint === b.relativeHumidity?.setpoint;
} else {
throw `Unknown steam mode ${a.mode}`;
}
Expand All @@ -31,6 +33,8 @@ const temperatureBulbsEqual = (a: TemperatureBulbSetPoint, b: TemperatureBulbSet
}
if (a.mode === 'dry' && b.mode === 'dry') {
return a.dry.setpoint.celsius === b.dry.setpoint.celsius;
} else if(a.mode === 'wet' && b.mode === 'wet'){
return a.wet.setpoint.celsius === b.wet.setpoint.celsius;
} else {
throw `Unknown temp bulb mode ${a.mode}`;
}
Expand All @@ -45,7 +49,7 @@ const stagesEqual = (stage1: StagesEntity, stage2: StagesEntity) => {
stage1.vent.open === stage2.vent.open;
};

export class AnovaOven extends (EventEmitter as new () => TypedEventEmitter<AnovaOvenResponse>) {
export class AnovaOven extends (EventEmitter as new () => TypedEventEmitter<AnovaOvenEvent>) {
private _id: DeviceID;
private _name: string;
private _curStateMessage: OvenStateMessage;
Expand All @@ -56,12 +60,25 @@ export class AnovaOven extends (EventEmitter as new () => TypedEventEmitter<Anov
this._id = id;
this._name = name;
this.on('ovenState', (update) => {
const prevCook = this._curStateMessage.cook;
this._curStateMessage = update;
if (update.cook && !prevCook) {
this._service.log.info(`Oven ${this._id} started cooking`);
this.emit('cookStart', update.cook);
} else if (prevCook && !update.cook) {
this._service.log.info(`Oven ${this._id} stopped cooking`);
this.emit('cookEnd');
}
});
this._curStateMessage = startingState;
this._service = service;
}

public set name(name: string) {
this.emit('setName', name);
this._name = name;
}

public get name() {
return this._name;
}
Expand Down Expand Up @@ -125,7 +142,7 @@ export class AnovaOven extends (EventEmitter as new () => TypedEventEmitter<Anov
* @param stages
*/
public isCooking(stages: StagesEntity[]) {
const curCook = this._curStateMessage.cook;
const curCook = this._curStateMessage?.cook;
if (!curCook) {
return false;
}
Expand Down Expand Up @@ -328,20 +345,27 @@ export default class AnovaOvenService extends (EventEmitter as new () => TypedEv
private _dispatchToOven(message: OvenResponse) {
if (message.command === 'EVENT_APO_WIFI_LIST') {
message.payload.forEach((oven) => {
this._pendingOvens.push({ cookerId: oven.cookerId, name: oven.name });
const existingOven = this.ovens.get(oven.cookerId);
if (existingOven) {
existingOven.name = oven.name;
} else {
this._pendingOvens.push({ cookerId: oven.cookerId, name: oven.name });
}
});
} else if (message.command === 'EVENT_APO_STATE') {
const ovenId = message.payload.cookerId;
this.log.info(`Oven ${ovenId} state changed`);
let oven = this.ovens.get(ovenId);
if (!oven) {
this.log.info(`Oven ${ovenId} not found, creating new`);
const ovenName = this._pendingOvens.find((eachOven) => eachOven.cookerId === ovenId)?.name;
oven = new AnovaOven(ovenId, ovenName || `Oven ${ovenId.substring(0, 4)}`, message.payload.state, this);
this.ovens.set(ovenId, oven);
this.emit('ovenFound', oven);
}
oven.curStateMessage = message.payload.state;
} else if (message.command === 'RESPONSE') {
this.log.info(`Received response to command ${message.requestId}`);
this.emit('commandResponse', message);
} else {
this.log.warn(`Unknown message type received: ${(message as { command: string }).command}`);
Expand Down
8 changes: 6 additions & 2 deletions src/AnovaTypes.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
export type AnovaOvenResponse = {
export type AnovaOvenEvent = {
setName: (newName: string) => void;
ovenState: (update: OvenStateMessage) => void;
cookStart: (cook: OvenStateMessage['cook']) => void;
cookEnd: () => void;
};
export type DeviceID = string;

Expand Down Expand Up @@ -108,7 +111,8 @@ export interface SteamGenerators {
steamPercentage?: SetpointNumber;
}
export interface RelativeHumidity {
current: number;
current?: number;
setpoint: number;
}
export interface Evaporator {
failed: boolean;
Expand Down
5 changes: 1 addition & 4 deletions src/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,10 @@ export class AnovaOvenHomebridgePlatform implements DynamicPlatformPlugin {
// the cached devices we stored in the `configureAccessory` method above
const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid);
if (existingAccessory) {
// this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [existingAccessory]);
// the accessory already exists
// existingAccessory.displayName = oven.name;
this.log.info('Restoring existing accessory from cache:', existingAccessory.displayName);
new AnovaOvenPlatformAccessory(this, existingAccessory, oven, this.config.recipes);
this.api.updatePlatformAccessories([existingAccessory]);

oven.name = `${oven.name}`;
} else {
// the accessory does not yet exist, so we need to create it
this.log.info(`Adding new accessory: ${oven.deviceId} ${oven.name}`);
Expand Down
23 changes: 23 additions & 0 deletions src/platformAccessory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { StagesEntity } from './AnovaTypes';
import { AnovaOvenHomebridgePlatform } from './platform';

export class AnovaOvenPlatformAccessory {

constructor(
readonly platform: AnovaOvenHomebridgePlatform,
readonly accessory: PlatformAccessory,
Expand Down Expand Up @@ -127,6 +128,28 @@ export class AnovaOvenPlatformAccessory {

const powerOnButton = createSwitchForRecipe({ name: 'Power On', stages: POWER_ON_DEFAULT_STAGES });
powerOnButton.setPrimaryService(true);

oven.on('ovenState', (update) => {
const prevCook = this.oven.curStateMessage?.cook;
if (update.cook && !prevCook) {
powerOnButton.updateCharacteristic(this.platform.Characteristic.On, true);
} else if (!update.cook && prevCook) {
powerOnButton.updateCharacteristic(this.platform.Characteristic.On, false);
}
});

oven.on('setName', (name) => {
const accessoryInformation = this.accessory.getService(this.platform.Service.AccessoryInformation);
if (accessoryInformation) {
this.platform.log.debug('Setting name to', name);
this.accessory.displayName = name;
//TODO - this doesn't seem to work
accessoryInformation.setCharacteristic(this.platform.Characteristic.Name, name);
accessoryInformation.setCharacteristic(this.platform.Characteristic.ConfiguredName, name);
}
this.platform.api.updatePlatformAccessories([this.accessory]);
});

recipes.forEach(createSwitchForRecipe);
}
}

0 comments on commit b7bc396

Please sign in to comment.