From 6f33993c3090f4a5ea14cb821a8450f6a4370241 Mon Sep 17 00:00:00 2001 From: Jeff Winn <6961614+jeff-winn@users.noreply.github.com> Date: Sat, 10 Aug 2024 21:36:06 -0400 Subject: [PATCH] Fixing issue with reporting incorrect state when out of battery (#360) * Refactoring converter to pass additional data for state conversion. * Modifying stopped to always result as faulted. * Changing behavior to fault when stopped in garden. * Improving fault hits for stopped in garden. * Adding fault detection when battery has been depleted. * Adding logging configuration support. * Cleanup. --- config.schema.json | 30 ++++++ src/clients/automower/automowerClient.ts | 33 ++++--- .../automower/automowerEventStreamClient.ts | 9 +- .../automower/automowerEventStreamService.ts | 2 +- .../automowerMowerStateConverter.ts | 42 ++++---- .../automowerEventStreamService.spec.ts | 2 +- .../automowerMowerStateConverter.spec.ts | 97 ++++++++++++++++--- 7 files changed, 164 insertions(+), 51 deletions(-) diff --git a/config.schema.json b/config.schema.json index 650a7f6d..8caece50 100644 --- a/config.schema.json +++ b/config.schema.json @@ -70,6 +70,35 @@ ] } }, + "logger_type": { + "title": "Logging", + "type": "string", + "description": "This determines how your plugin logs statements. The default logger doesn't do debugging.", + "required": true, + "default": "default", + "enum": [ + "default", + "imitation", + "force_debug" + ], + "x-schema-form": { + "type": "select", + "titleMap": [ + { + "value": "default", + "name": "Default - Homebridge stock logger" + }, + { + "value": "imitation", + "name": "Imitation - Looks like the default logger but includes debugging support" + }, + { + "value": "force_debug", + "name": "Force debug statements" + } + ] + } + }, "username": { "title": "Email Address", "type": "string", @@ -187,6 +216,7 @@ }, "device_type", "sensor_mode", + "logger_type", { "type": "fieldset", "title": "Account Settings", diff --git a/src/clients/automower/automowerClient.ts b/src/clients/automower/automowerClient.ts index 8c5b3000..b896509d 100644 --- a/src/clients/automower/automowerClient.ts +++ b/src/clients/automower/automowerClient.ts @@ -9,17 +9,28 @@ import { FetchClient, Response } from '../fetchClient'; export interface Mower { type: string; id: string; - attributes: { - system: Device; - battery: Battery; - mower: MowerState; - calendar: Calendar; - planner: Planner; - metadata: MowerMetadata; - positions: Position[]; - settings: Settings; - statistics: Statistics; - }; + attributes: MowerAttributes; +} + +/** + * Describes the mower attributes. + */ +export interface MowerAttributes extends StatusAttributes { + system: Device; + calendar: Calendar; + positions: Position[]; + settings: Settings; + statistics: Statistics; +} + +/** + * Describes status attributes. + */ +export interface StatusAttributes { + battery: Battery; + mower: MowerState; + planner: Planner; + metadata: MowerMetadata; } /** diff --git a/src/clients/automower/automowerEventStreamClient.ts b/src/clients/automower/automowerEventStreamClient.ts index da468975..3ad8d481 100644 --- a/src/clients/automower/automowerEventStreamClient.ts +++ b/src/clients/automower/automowerEventStreamClient.ts @@ -2,7 +2,7 @@ import { PlatformLogger } from '../../diagnostics/platformLogger'; import { AccessToken } from '../../model'; import { WebSocketWrapper } from '../../primitives/webSocketWrapper'; import { AbstractEventStreamClient, EventStreamClient } from '../eventStreamClient'; -import { Battery, Calendar, Headlight, MowerMetadata, MowerState, Planner, Position } from './automowerClient'; +import { Calendar, Headlight, Position, StatusAttributes } from './automowerClient'; /** * Describes a connected event. @@ -34,12 +34,7 @@ export enum AutomowerEventTypes { * Describes a status event. */ export interface StatusEvent extends AutomowerEvent { - attributes: { - battery: Battery; - mower: MowerState; - planner: Planner; - metadata: MowerMetadata; - }; + attributes: StatusAttributes; } /** diff --git a/src/services/husqvarna/automower/automowerEventStreamService.ts b/src/services/husqvarna/automower/automowerEventStreamService.ts index bf9ac31c..67df0af3 100644 --- a/src/services/husqvarna/automower/automowerEventStreamService.ts +++ b/src/services/husqvarna/automower/automowerEventStreamService.ts @@ -88,7 +88,7 @@ export class AutomowerEventStreamService extends AbstractEventStreamService { } }; - stateConverter.setup(o => o.convertMowerState(mowerState)).returns({ + stateConverter.setup(o => o.convertStatusAttributes(event.attributes)).returns({ activity: model.Activity.MOWING, state: model.State.IN_OPERATION }); diff --git a/test/services/husqvarna/automower/converters/automowerMowerStateConverter.spec.ts b/test/services/husqvarna/automower/converters/automowerMowerStateConverter.spec.ts index 0d44dd72..a1e1e3d1 100644 --- a/test/services/husqvarna/automower/converters/automowerMowerStateConverter.spec.ts +++ b/test/services/husqvarna/automower/converters/automowerMowerStateConverter.spec.ts @@ -17,6 +17,78 @@ describe('AutomowerMowerStateConverterImpl', () => { target = new AutomowerMowerStateConverterImpl(log.object()); }); + it('should return faulted when battery has been depleted while mowing main area', () => { + const mower: StatusEvent = { + id: '12345', + type: AutomowerEventTypes.STATUS, + attributes:{ + battery:{ + batteryPercent: 0 + }, + mower:{ + mode: Mode.MAIN_AREA, + activity: Activity.NOT_APPLICABLE, + state: State.PAUSED, + errorCode: 0, + errorCodeTimestamp: 0 + }, + planner:{ + nextStartTimestamp: 0, + override:{ + action: OverrideAction.NOT_ACTIVE + }, + restrictedReason: RestrictedReason.NOT_APPLICABLE + }, + metadata:{ + connected: true, + statusTimestamp: 1723315112510 + } + } + }; + + const result = target.convertStatusAttributes(mower.attributes); + + expect(result).toBeDefined(); + expect(result.activity).toEqual(model.Activity.MOWING); + expect(result.state).toEqual(model.State.FAULTED); + }); + + it('should return faulted when battery has been depleted while mowing secondary area', () => { + const mower: StatusEvent = { + id: '12345', + type: AutomowerEventTypes.STATUS, + attributes:{ + battery:{ + batteryPercent: 0 + }, + mower:{ + mode: Mode.SECONDARY_AREA, + activity: Activity.NOT_APPLICABLE, + state: State.PAUSED, + errorCode: 0, + errorCodeTimestamp: 0 + }, + planner:{ + nextStartTimestamp: 0, + override:{ + action: OverrideAction.NOT_ACTIVE + }, + restrictedReason: RestrictedReason.NOT_APPLICABLE + }, + metadata:{ + connected: true, + statusTimestamp: 1723315112510 + } + } + }; + + const result = target.convertStatusAttributes(mower.attributes); + + expect(result).toBeDefined(); + expect(result.activity).toEqual(model.Activity.MOWING); + expect(result.state).toEqual(model.State.FAULTED); + }); + it('should return parked and idle when parked and outside of scheduled window', () => { const mower: StatusEvent = { id: '12345', @@ -41,11 +113,12 @@ describe('AutomowerMowerStateConverterImpl', () => { }, metadata:{ connected: true, - statusTimestamp: 1723071500034} + statusTimestamp: 1723071500034 } - }; + } + }; - const result = target.convertMowerState(mower.attributes.mower); + const result = target.convertStatusAttributes(mower.attributes); expect(result).toBeDefined(); expect(result.activity).toEqual(model.Activity.PARKED); @@ -616,7 +689,7 @@ describe('AutomowerMowerStateConverterImpl', () => { expect(result.state).toEqual(model.State.LEAVING_HOME); }); - it('should return mowing when stopped in garden', () => { + it('should return faulted when stopped in garden', () => { const mower: Mower = { id: '12345', type: 'mower', @@ -629,7 +702,7 @@ describe('AutomowerMowerStateConverterImpl', () => { activity: Activity.STOPPED_IN_GARDEN, errorCode: 0, errorCodeTimestamp: 0, - mode: Mode.MAIN_AREA, + mode: Mode.SECONDARY_AREA, state: State.UNKNOWN }, planner: { @@ -682,7 +755,7 @@ describe('AutomowerMowerStateConverterImpl', () => { expect(result).toBeDefined(); expect(result.activity).toEqual(model.Activity.MOWING); - expect(result.state).toEqual(model.State.UNKNOWN); + expect(result.state).toEqual(model.State.FAULTED); }); it('should return mowing when mowing', () => { @@ -825,7 +898,7 @@ describe('AutomowerMowerStateConverterImpl', () => { log.verify(o => o.debug(It.IsAny(), It.IsAny()), Times.Once()); }); - it('should return tampered when stopped with error', () => { + it('should return faulted when stopped with error', () => { const mower: Mower = { id: '12345', type: 'mower', @@ -890,10 +963,10 @@ describe('AutomowerMowerStateConverterImpl', () => { const result = target.convertMower(mower); expect(result).toBeDefined(); - expect(result.state).toEqual(model.State.TAMPERED); + expect(result.state).toEqual(model.State.FAULTED); }); - it('should return charging while mowing secondary area', () => { + it('should return charging while mowing secondary area and battery has been depleted', () => { const mower: Mower = { id: '12345', type: 'mower', @@ -935,7 +1008,7 @@ describe('AutomowerMowerStateConverterImpl', () => { serialNumber: 1 }, battery: { - batteryPercent: 100 + batteryPercent: 80 }, calendar: { tasks: [ @@ -962,7 +1035,7 @@ describe('AutomowerMowerStateConverterImpl', () => { expect(result.state).toEqual(model.State.CHARGING); }); - it('should return charging while mowing main area', () => { + it('should return charging while mowing main area and battery has been depleted', () => { const mower: Mower = { id: '12345', type: 'mower', @@ -1004,7 +1077,7 @@ describe('AutomowerMowerStateConverterImpl', () => { serialNumber: 1 }, battery: { - batteryPercent: 100 + batteryPercent: 80 }, calendar: { tasks: [