diff --git a/plugins/homekit/src/types/airpurifier.ts b/plugins/homekit/src/types/airpurifier.ts new file mode 100644 index 0000000000..2a0aea5ac9 --- /dev/null +++ b/plugins/homekit/src/types/airpurifier.ts @@ -0,0 +1,119 @@ +import { ScryptedDevice, ScryptedDeviceType, ScryptedInterface, AirPurifierStatus, AirPurifierMode, AirPurifier, FilterMaintenance } from '@scrypted/sdk'; +import { addSupportedType, bindCharacteristic, DummyDevice, } from '../common'; +import { Characteristic, CharacteristicEventTypes, CharacteristicSetCallback, CharacteristicValue, Service } from '../hap'; +import { makeAccessory } from './common'; +import type { HomeKitPlugin } from "../main"; + +addSupportedType({ + type: ScryptedDeviceType.AirPurifier, + probe(device: DummyDevice): boolean { + return device.interfaces.includes(ScryptedInterface.AirPurifier); + }, + getAccessory: async (device: ScryptedDevice & AirPurifier & FilterMaintenance, homekitPlugin: HomeKitPlugin) => { + const accessory = makeAccessory(device, homekitPlugin); + + const service = accessory.addService(Service.AirPurifier, device.name); + const nightModeService = accessory.addService(Service.Switch, `${device.name} Night Mode`) + + /* On/Off AND mode toggle */ + bindCharacteristic(device, ScryptedInterface.AirPurifier, service, Characteristic.Active, + () => { + switch(device.airPurifierState.status) { + case AirPurifierStatus.Active: + return Characteristic.Active.ACTIVE; + case AirPurifierStatus.ActiveNightMode: + return Characteristic.Active.ACTIVE; + } + return Characteristic.Active.INACTIVE; + }); + + service.getCharacteristic(Characteristic.Active) + .on(CharacteristicEventTypes.SET, (value: CharacteristicValue, callback: CharacteristicSetCallback) => { + callback(); + device.setAirPurifierState({ + status: (value as boolean) ? AirPurifierStatus.Active : AirPurifierStatus.Inactive, + }) + }); + + /* Current State */ + bindCharacteristic(device, ScryptedInterface.AirPurifier, service, Characteristic.CurrentAirPurifierState, + () => { + switch (device.airPurifierState.status) { + case AirPurifierStatus.Inactive: + return Characteristic.CurrentAirPurifierState.INACTIVE; + case AirPurifierStatus.Idle: + return Characteristic.CurrentAirPurifierState.IDLE; + } + return Characteristic.CurrentAirPurifierState.PURIFYING_AIR; + }); + + /* Fan Speed */ + bindCharacteristic(device, ScryptedInterface.AirPurifier, service, Characteristic.RotationSpeed, + () => device.airPurifierState.speed); + + service.getCharacteristic(Characteristic.RotationSpeed) + .on(CharacteristicEventTypes.SET, (value: CharacteristicValue, callback: CharacteristicSetCallback) => { + callback(); + device.setAirPurifierState({ + speed: value, + }) + }) + + /* i.e. Mode: Manual/Auto slider */ + bindCharacteristic(device, ScryptedInterface.AirPurifier, service, Characteristic.TargetAirPurifierState, + () => { + if (device.airPurifierState.mode == AirPurifierMode.Automatic) + return Characteristic.TargetAirPurifierState.AUTO; + return Characteristic.TargetAirPurifierState.MANUAL; + }); + + service.getCharacteristic(Characteristic.TargetAirPurifierState) + .on(CharacteristicEventTypes.SET, (value: CharacteristicValue, callback: CharacteristicSetCallback) => { + callback(); + device.setAirPurifierState({ + mode: value === Characteristic.TargetAirPurifierState.AUTO ? AirPurifierMode.Automatic : AirPurifierMode.Manual, + }) + }); + + /* LockPhysicalControls i.e. "Child Lock: Unlocked/Locked" */ + bindCharacteristic(device, ScryptedInterface.AirPurifier, service, Characteristic.LockPhysicalControls, + () => !!device.airPurifierState.lockPhysicalControls); + + service.getCharacteristic(Characteristic.LockPhysicalControls) + .on(CharacteristicEventTypes.SET, (value: CharacteristicValue, callback: CharacteristicSetCallback) => { + callback(); + device.setAirPurifierState({ + lockPhysicalControls: (value as boolean), + }) + }) + + /* Night mode switch */ + bindCharacteristic(device, ScryptedInterface.AirPurifier, nightModeService, Characteristic.On, + () => !!(device.airPurifierState.status === AirPurifierStatus.ActiveNightMode)); + + nightModeService.getCharacteristic(Characteristic.On) + .on(CharacteristicEventTypes.SET, (value: CharacteristicValue, callback: CharacteristicSetCallback) => { + callback(); + device.setAirPurifierState({ + status: value ? AirPurifierStatus.ActiveNightMode : AirPurifierStatus.Active, + }) + }) + + /* Optional: Filter Maintenance Service */ + if (device.interfaces.includes(ScryptedInterface.FilterMaintenance)) { + const filterMaintenanceService = accessory.addService(Service.FilterMaintenance, device.name); + + bindCharacteristic(device, ScryptedInterface.FilterMaintenance, filterMaintenanceService, Characteristic.FilterLifeLevel, + () => device.filterLifeLevel) + + bindCharacteristic(device, ScryptedInterface.FilterMaintenance, filterMaintenanceService, Characteristic.FilterChangeIndication, + () => { + if (device.filterChangeIndication) + return Characteristic.FilterChangeIndication.CHANGE_FILTER; + return Characteristic.FilterChangeIndication.FILTER_OK; + }) + } + + return accessory; + } +}); diff --git a/plugins/homekit/src/types/index.ts b/plugins/homekit/src/types/index.ts index c40cadfab5..38e7282bc2 100644 --- a/plugins/homekit/src/types/index.ts +++ b/plugins/homekit/src/types/index.ts @@ -15,3 +15,4 @@ import './vacuum'; import './outlet'; import './notifier'; import './windowcovering' +import './airpurifier' diff --git a/sdk/types/scrypted_python/scrypted_sdk/types.py b/sdk/types/scrypted_python/scrypted_sdk/types.py index 94264c73d8..b7dccba9f6 100644 --- a/sdk/types/scrypted_python/scrypted_sdk/types.py +++ b/sdk/types/scrypted_python/scrypted_sdk/types.py @@ -9,6 +9,16 @@ from .other import * +class AirPurifierMode(Enum): + Automatic = "Automatic" + Manual = "Manual" + +class AirPurifierStatus(Enum): + Active = "Active" + ActiveNightMode = "ActiveNightMode" + Idle = "Idle" + Inactive = "Inactive" + class AirQuality(Enum): Excellent = "Excellent" Fair = "Fair" @@ -49,6 +59,7 @@ class PanTiltZoomMovement(Enum): class ScryptedDeviceType(Enum): API = "API" + AirPurifier = "AirPurifier" Automation = "Automation" Builtin = "Builtin" Camera = "Camera" @@ -83,6 +94,7 @@ class ScryptedDeviceType(Enum): WindowCovering = "WindowCovering" class ScryptedInterface(Enum): + AirPurifier = "AirPurifier" AirQualitySensor = "AirQualitySensor" AmbientLightSensor = "AmbientLightSensor" AudioSensor = "AudioSensor" @@ -106,6 +118,7 @@ class ScryptedInterface(Enum): EntrySensor = "EntrySensor" EventRecorder = "EventRecorder" Fan = "Fan" + FilterMaintenance = "FilterMaintenance" FloodSensor = "FloodSensor" HttpRequestHandler = "HttpRequestHandler" HumiditySensor = "HumiditySensor" @@ -319,6 +332,13 @@ class AdoptDevice(TypedDict): settings: DeviceCreatorSettings pass +class AirPurifierState(TypedDict): + lockPhysicalControls: bool + mode: AirPurifierMode + speed: float + status: AirPurifierStatus + pass + class ColorHsv(TypedDict): h: float s: float @@ -733,6 +753,12 @@ class VideoFrameGeneratorOptions(TypedDict): class TamperState(TypedDict): pass +class AirPurifier: + airPurifierState: AirPurifierState + async def setAirPurifierState(self, state: AirPurifierState) -> None: + pass + pass + class AirQualitySensor: airQuality: AirQuality pass @@ -864,6 +890,11 @@ async def setFan(self, fan: FanState) -> None: pass pass +class FilterMaintenance: + filterChangeIndication: bool + filterLifeLevel: float + pass + class FloodSensor: flooded: bool pass @@ -1400,6 +1431,9 @@ class ScryptedInterfaceProperty(Enum): noxDensity = "noxDensity" co2ppm = "co2ppm" airQuality = "airQuality" + airPurifierState = "airPurifierState" + filterChangeIndication = "filterChangeIndication" + filterLifeLevel = "filterLifeLevel" humiditySetting = "humiditySetting" fan = "fan" applicationInfo = "applicationInfo" @@ -1481,6 +1515,7 @@ class ScryptedInterfaceMethods(Enum): putSetting = "putSetting" armSecuritySystem = "armSecuritySystem" disarmSecuritySystem = "disarmSecuritySystem" + setAirPurifierState = "setAirPurifierState" getReadmeMarkdown = "getReadmeMarkdown" getOauthUrl = "getOauthUrl" onOauthCallback = "onOauthCallback" @@ -1912,6 +1947,27 @@ def airQuality(self) -> AirQuality: def airQuality(self, value: AirQuality): self.setScryptedProperty("airQuality", value) + @property + def airPurifierState(self) -> AirPurifierState: + return self.getScryptedProperty("airPurifierState") + @airPurifierState.setter + def airPurifierState(self, value: AirPurifierState): + self.setScryptedProperty("airPurifierState", value) + + @property + def filterChangeIndication(self) -> bool: + return self.getScryptedProperty("filterChangeIndication") + @filterChangeIndication.setter + def filterChangeIndication(self, value: bool): + self.setScryptedProperty("filterChangeIndication", value) + + @property + def filterLifeLevel(self) -> float: + return self.getScryptedProperty("filterLifeLevel") + @filterLifeLevel.setter + def filterLifeLevel(self, value: float): + self.setScryptedProperty("filterLifeLevel", value) + @property def humiditySetting(self) -> HumiditySettingStatus: return self.getScryptedProperty("humiditySetting") @@ -2431,6 +2487,23 @@ def applicationInfo(self, value: LauncherApplicationInfo): "airQuality" ] }, + "AirPurifier": { + "name": "AirPurifier", + "methods": [ + "setAirPurifierState" + ], + "properties": [ + "airPurifierState" + ] + }, + "FilterMaintenance": { + "name": "FilterMaintenance", + "methods": [], + "properties": [ + "filterChangeIndication", + "filterLifeLevel" + ] + }, "Readme": { "name": "Readme", "methods": [ diff --git a/sdk/types/src/types.input.ts b/sdk/types/src/types.input.ts index 22c68e3bda..3a61e29998 100644 --- a/sdk/types/src/types.input.ts +++ b/sdk/types/src/types.input.ts @@ -131,6 +131,7 @@ export enum ScryptedDeviceType { SecuritySystem = "SecuritySystem", WindowCovering = "WindowCovering", Siren = "Siren", + AirPurifier = "AirPurifier", Unknown = "Unknown", } /** @@ -1181,6 +1182,36 @@ export interface Position { export interface PositionSensor { position?: Position; } +export enum AirPurifierStatus { + Inactive = "Inactive", + Idle = "Idle", + Active = "Active", + ActiveNightMode = "ActiveNightMode", +} + +export enum AirPurifierMode { + Manual = "Manual", + Automatic = "Automatic", +} + +export interface AirPurifierState { + speed?: number; + status?: AirPurifierStatus, + mode?: AirPurifierMode, + lockPhysicalControls?: boolean, +} + +export interface AirPurifier { + airPurifierState?: AirPurifierState; + + setAirPurifierState(state: AirPurifierState): Promise; +} + +export interface FilterMaintenance { + filterLifeLevel?: number, + filterChangeIndication?: boolean, +} + export interface PM10Sensor { pm10Density?: number; } @@ -1950,6 +1981,8 @@ export enum ScryptedInterface { NOXSensor = "NOXSensor", CO2Sensor = "CO2Sensor", AirQualitySensor = "AirQualitySensor", + AirPurifier = "AirPurifier", + FilterMaintenance = "FilterMaintenance", Readme = "Readme", OauthClient = "OauthClient", MixinProvider = "MixinProvider",