diff --git a/src/extensions/domains/camera.extension.ts b/src/extensions/domains/camera.extension.ts new file mode 100644 index 0000000..bb211fd --- /dev/null +++ b/src/extensions/domains/camera.extension.ts @@ -0,0 +1,175 @@ +import { is, TServiceParams } from "@digital-alchemy/core"; + +import { + CameraConfiguration, + RemovableCallback, + SynapseCameraParams, + SynapseVirtualCamera, + VIRTUAL_ENTITY_BASE_KEYS, +} from "../../helpers"; +import { TRegistry } from "../registry.extension"; + +export function VirtualCamera({ context, synapse }: TServiceParams) { + const registry = synapse.registry.create({ + context, + // @ts-expect-error it's fine + domain: "fan", + }); + + // #MARK: create + return function < + STATE extends string = string, + ATTRIBUTES extends object = object, + >(entity: SynapseCameraParams) { + const proxy = new Proxy({} as SynapseVirtualCamera, { + // #MARK: get + // eslint-disable-next-line sonarjs/cognitive-complexity + get(_, property: keyof SynapseVirtualCamera) { + // > common + // * name + if (property === "name") { + return entity.name; + } + // * unique_id + if (property === "unique_id") { + return unique_id; + } + // * onUpdate + if (property === "onUpdate") { + return loader.onUpdate(); + } + // * _rawConfiguration + if (property === "_rawConfiguration") { + return loader.configuration; + } + // * _rawAttributes + if (property === "_rawAttributes") { + return loader.attributes; + } + // * attributes + if (property === "attributes") { + return loader.attributesProxy(); + } + // * configuration + if (property === "configuration") { + return loader.configurationProxy(); + } + // * state + if (property === "state") { + return loader.state; + } + // > domain specific + // * onTurnOn + if (property === "onTurnOn") { + return (callback: RemovableCallback) => + synapse.registry.removableListener(TURN_ON, callback); + } + // * onTurnOff + if (property === "onTurnOff") { + return (callback: RemovableCallback) => + synapse.registry.removableListener(TURN_OFF, callback); + } + // * onEnableMotionDetection + if (property === "onEnableMotionDetection") { + return (callback: RemovableCallback) => + synapse.registry.removableListener( + ENABLE_MOTION_DETECTION, + callback, + ); + } + // * onDisableMotionDetection + if (property === "onDisableMotionDetection") { + return (callback: RemovableCallback) => + synapse.registry.removableListener( + DISABLE_MOTION_DETECTION, + callback, + ); + } + return undefined; + }, + + ownKeys: () => [...VIRTUAL_ENTITY_BASE_KEYS, "onSetValue"], + + // #MARK: set + set(_, property: string, value: unknown) { + // > common + // * state + if (property === "state") { + loader.setState(value as STATE); + return true; + } + // * attributes + if (property === "attributes") { + loader.setAttributes(value as ATTRIBUTES); + return true; + } + return false; + }, + }); + + // - Add to registry + const unique_id = registry.add(proxy, entity); + + // - Initialize value storage + const loader = synapse.storage.wrapper< + STATE, + ATTRIBUTES, + CameraConfiguration + >({ + load_keys: [ + "brand", + "frame_interval", + "frontend_stream_type", + "is_on", + "is_recording", + "is_streaming", + "model", + "motion_detection_enabled", + "use_stream_for_stills", + "supported_features", + ], + name: entity.name, + registry: registry as TRegistry, + unique_id, + }); + + // - Attach bus events + const TURN_ON = synapse.registry.busTransfer({ + context, + eventName: "turn_on", + unique_id, + }); + const TURN_OFF = synapse.registry.busTransfer({ + context, + eventName: "turn_off", + unique_id, + }); + const ENABLE_MOTION_DETECTION = synapse.registry.busTransfer({ + context, + eventName: "enable_motion_detection", + unique_id, + }); + const DISABLE_MOTION_DETECTION = synapse.registry.busTransfer({ + context, + eventName: "disable_motion_detection", + unique_id, + }); + + // - Attach static listener + if (is.function(entity.turn_on)) { + proxy.onTurnOn(entity.turn_on); + } + if (is.function(entity.turn_off)) { + proxy.onTurnOff(entity.turn_off); + } + if (is.function(entity.enable_motion_detection)) { + proxy.onEnableMotionDetection(entity.enable_motion_detection); + } + if (is.function(entity.disable_motion_detection)) { + proxy.onDisableMotionDetection(entity.disable_motion_detection); + } + + // - Done + return proxy; + }; +} diff --git a/src/extensions/domains/index.ts b/src/extensions/domains/index.ts index b2fbaa3..fcddee1 100644 --- a/src/extensions/domains/index.ts +++ b/src/extensions/domains/index.ts @@ -1,6 +1,7 @@ export * from "./alarm-control-panel.extension"; export * from "./binary-sensor.extension"; export * from "./button.extension"; +export * from "./camera.extension"; export * from "./climate.extension"; export * from "./cover.extension"; export * from "./date.extension"; diff --git a/src/helpers/domains/camera.ts b/src/helpers/domains/camera.ts index dbc9470..4abd797 100644 --- a/src/helpers/domains/camera.ts +++ b/src/helpers/domains/camera.ts @@ -1,44 +1,41 @@ -import { TContext } from "@digital-alchemy/core"; +import { + BaseEntityParams, + BaseVirtualEntity, + CreateRemovableCallback, + RemovableCallback, +} from "../base-domain.helper"; +import { EntityConfigCommon } from "../common-config.helper"; -import { BASE_CONFIG_KEYS, EntityConfigCommon } from "../common-config.helper"; -import { TSynapseId } from "../utility.helper"; +export type SynapseCameraParams = BaseEntityParams & + CameraConfiguration & { + turn_on?: RemovableCallback; + turn_off?: RemovableCallback; + enable_motion_detection?: RemovableCallback; + disable_motion_detection?: RemovableCallback; + }; -export type TAlarmControlPanel< - STATE extends AlarmControlPanelValue, - ATTRIBUTES extends object = object, -> = { - context: TContext; - defaultState?: STATE; - defaultAttributes?: ATTRIBUTES; - name: string; -} & AlarmControlPanelConfiguration; +type CameraStates = "on" | "off"; -export type AlarmControlPanelConfiguration = EntityConfigCommon & { - code_arm_required?: boolean; - code_format?: "text" | "number"; +export type CameraConfiguration = EntityConfigCommon & { + brand?: string; + frame_interval?: number; + frontend_stream_type?: string; + is_on?: boolean; + is_recording?: boolean; + is_streaming?: boolean; + model?: string; + motion_detection_enabled?: boolean; + use_stream_for_stills?: boolean; supported_features?: number; - changed_by?: string; }; -export type AlarmControlPanelValue = - | "disarmed" - | "armed_home" - | "armed_away" - | "armed_night" - | "armed_vacation" - | "armed_custom_bypass" - | "pending" - | "arming" - | "disarming" - | "triggered"; - -export const ALARM_CONTROL_PANEL_CONFIGURATION_KEYS = [ - ...BASE_CONFIG_KEYS, - "device_class", -] as (keyof AlarmControlPanelConfiguration)[]; - -export type HassAlarmControlPanelEvent = { - data: { unique_id: TSynapseId; code: string }; +export type SynapseVirtualCamera = BaseVirtualEntity< + CameraStates, + object, + CameraConfiguration +> & { + onTurnOn?: CreateRemovableCallback; + onTurnOff?: CreateRemovableCallback; + onEnableMotionDetection?: CreateRemovableCallback; + onDisableMotionDetection?: CreateRemovableCallback; }; - -export type RemoveReturn = { remove: () => void }; diff --git a/src/helpers/domains/index.ts b/src/helpers/domains/index.ts index eeca453..9bd7fad 100644 --- a/src/helpers/domains/index.ts +++ b/src/helpers/domains/index.ts @@ -1,7 +1,7 @@ export * from "./alarm-control-panel"; export * from "./binary-sensor"; export * from "./button"; -// export * from "./camera"; +export * from "./camera"; export * from "./climate"; export * from "./cover"; export * from "./date"; diff --git a/src/synapse.module.ts b/src/synapse.module.ts index ddb2cad..bb08d75 100644 --- a/src/synapse.module.ts +++ b/src/synapse.module.ts @@ -12,6 +12,7 @@ import { VirtualAlarmControlPanel, VirtualBinarySensor, VirtualButton, + VirtualCamera, VirtualClimate, VirtualCover, VirtualDate, @@ -41,6 +42,7 @@ const DOMAINS = { alarm_control_panel: VirtualAlarmControlPanel, binary_sensor: VirtualBinarySensor, button: VirtualButton, + camera: VirtualCamera, climate: VirtualClimate, cover: VirtualCover, date: VirtualDate,