From 0fd0633b060cdb0a523f1e7da4ee43db0aedab7c Mon Sep 17 00:00:00 2001 From: Zoe Codez Date: Sat, 25 May 2024 12:23:20 -0500 Subject: [PATCH] carrying through refactors to button --- src/extensions/binary-sensor.extension.ts | 34 +++-- src/extensions/button.extension.ts | 164 ++++++---------------- src/extensions/registry.extension.ts | 55 +++++++- src/extensions/sensor.extension.ts | 159 ++++++++------------- src/extensions/storage.extension.ts | 14 +- src/helpers/domains/button.ts | 55 ++------ src/helpers/domains/sensor.ts | 45 +----- 7 files changed, 194 insertions(+), 332 deletions(-) diff --git a/src/extensions/binary-sensor.extension.ts b/src/extensions/binary-sensor.extension.ts index 5874778..8c4f596 100644 --- a/src/extensions/binary-sensor.extension.ts +++ b/src/extensions/binary-sensor.extension.ts @@ -12,11 +12,6 @@ import { export function VirtualBinarySensor({ context, synapse }: TServiceParams) { const registry = synapse.registry.create({ context, - details: entity => ({ - attributes: entity._rawAttributes, - configuration: entity._rawConfiguration, - state: entity.state, - }), domain: "binary_sensor", }); @@ -31,14 +26,7 @@ export function VirtualBinarySensor({ context, synapse }: TServiceParams) { const proxy = new Proxy({} as SynapseVirtualBinarySensor, { // #MARK: get get(_, property: keyof SynapseVirtualBinarySensor) { - // * state - if (property === "state") { - return loader.state; - } - // * is_on - if (property === "is_on") { - return loader.state === "on"; - } + // > common // * name if (property === "name") { return entity.name; @@ -67,6 +55,15 @@ export function VirtualBinarySensor({ context, synapse }: TServiceParams) { if (property === "configuration") { return loader.configurationProxy(); } + // > domain specific + // * state + if (property === "state") { + return loader.state; + } + // * is_on + if (property === "is_on") { + return loader.state === "on"; + } return undefined; }, @@ -74,6 +71,12 @@ export function VirtualBinarySensor({ context, synapse }: TServiceParams) { // #MARK: set set(_, property: string, value: unknown) { + // * attributes + if (property === "attributes") { + loader.setAttributes(value as ATTRIBUTES); + return true; + } + // > domain specific // * state if (property === "state") { loader.setState(value as STATE); @@ -85,11 +88,6 @@ export function VirtualBinarySensor({ context, synapse }: TServiceParams) { loader.setState(new_state); return true; } - // * attributes - if (property === "attributes") { - loader.setAttributes(value as ATTRIBUTES); - return true; - } return false; }, }); diff --git a/src/extensions/button.extension.ts b/src/extensions/button.extension.ts index 1155545..20ad502 100644 --- a/src/extensions/button.extension.ts +++ b/src/extensions/button.extension.ts @@ -1,46 +1,28 @@ import { is, TBlackHole, TServiceParams } from "@digital-alchemy/core"; -import { TRegistry } from ".."; +import { TRegistry, VIRTUAL_ENTITY_BASE_KEYS } from ".."; import { - BUTTON_CONFIGURATION_KEYS, ButtonConfiguration, - HassButtonUpdateEvent, - TButton, - TVirtualButton, + SynapseButtonParams, + SynapseVirtualButton, } from "../helpers/domains"; -export function VirtualButton({ - logger, - event, - hass, - context, - synapse, - internal, -}: TServiceParams) { - const registry = synapse.registry.create({ +export function VirtualButton({ context, synapse }: TServiceParams) { + const registry = synapse.registry.create({ context, - details: entity => ({ - attributes: entity._rawAttributes, - configuration: entity._rawConfiguration, - state: undefined, - }), // @ts-expect-error it's fine domain: "button", }); // #MARK: create - function create< - STATE extends void = void, - ATTRIBUTES extends object = object, - CONFIGURATION extends ButtonConfiguration = ButtonConfiguration, - >(entity: TButton) { - const entityOut = new Proxy({} as TVirtualButton, { + return function ( + entity: SynapseButtonParams, + ) { + // - Define the proxy + const proxy = new Proxy({} as SynapseVirtualButton, { // #MARK: get - get(_, property: keyof TVirtualButton) { - // * state - if (property === "state") { - return undefined; - } + get(_, property: keyof SynapseVirtualButton) { + // > common // * name if (property === "name") { return entity.name; @@ -53,16 +35,6 @@ export function VirtualButton({ if (property === "onUpdate") { return loader.onUpdate(); } - // * onPress - if (property === "onPress") { - return function (callback: (remove: () => void) => TBlackHole) { - const remove = () => event.removeListener(EVENT_ID, exec); - const exec = async () => - await internal.safeExec(async () => callback(remove)); - event.on(EVENT_ID, exec); - return { remove }; - }; - } // * _rawConfiguration if (property === "_rawConfiguration") { return loader.configuration; @@ -73,64 +45,27 @@ export function VirtualButton({ } // * attributes if (property === "attributes") { - return new Proxy({} as ATTRIBUTES, { - get: >( - _: ATTRIBUTES, - property: KEY, - ) => { - return loader.attributes[property]; - }, - set: < - KEY extends Extract, - VALUE extends ATTRIBUTES[KEY], - >( - _: ATTRIBUTES, - property: KEY, - value: VALUE, - ) => { - loader.setAttribute(property, value); - return true; - }, - }); + return loader.attributesProxy(); } // * configuration if (property === "configuration") { - return new Proxy({} as CONFIGURATION, { - get: >( - _: CONFIGURATION, - property: KEY, - ) => { - return loader.configuration[property]; - }, - set: < - KEY extends Extract, - VALUE extends CONFIGURATION[KEY], - >( - _: CONFIGURATION, - property: KEY, - value: VALUE, - ) => { - loader.setConfiguration(property, value); - return true; - }, - }); + return loader.configurationProxy(); + } + // > domain specific + // * onPress + if (property === "onPress") { + return (callback: (remove: () => void) => TBlackHole) => + synapse.registry.removableListener(PRESS_EVENT, callback); } return undefined; }, - // #MARK: ownKeys - ownKeys: () => { - return [ - "attributes", - "configuration", - "_rawAttributes", - "_rawConfiguration", - "name", - "onUpdate", - "onPress", - ]; - }, + + ownKeys: () => [...VIRTUAL_ENTITY_BASE_KEYS, "onPress"], + // #MARK: set set(_, property: string, value: unknown) { + // > common + // * attributes if (property === "attributes") { loader.setAttributes(value as ATTRIBUTES); return true; @@ -139,45 +74,34 @@ export function VirtualButton({ }, }); - // Validate a good id was passed, and it's the only place in code that's using it - const unique_id = registry.add(entityOut, entity); - const EVENT_ID = `synapse/press/${unique_id}`; + // - Add to registry + const unique_id = registry.add(proxy, entity); - const loader = synapse.storage.wrapper({ + // - Initialize value storage + const loader = synapse.storage.wrapper< + never, + ATTRIBUTES, + ButtonConfiguration + >({ + load_keys: ["device_class"], name: entity.name, registry: registry as TRegistry, unique_id, - value: { - attributes: (entity.defaultAttributes ?? {}) as ATTRIBUTES, - configuration: Object.fromEntries( - BUTTON_CONFIGURATION_KEYS.map(key => [key, entity[key]]), - ) as unknown as CONFIGURATION, - state: undefined, - }, }); - logger.error({ event }, `listening for event`); - - hass.socket.onEvent({ + // - Attach bus events + const PRESS_EVENT = synapse.registry.busTransfer({ context, - event: synapse.registry.eventName("press"), - async exec({ data: { unique_id: id } }: HassButtonUpdateEvent) { - if (id !== unique_id) { - return; - } - logger.trace({ context, name: entity.name }, `press`); - event.emit(EVENT_ID); - }, + eventName: "press", + unique_id, }); + + // - Attach static listener if (is.function(entity.press)) { - const remove = () => event.removeListener(EVENT_ID, callback); - const callback = async () => - await internal.safeExec(async () => entity.press(remove)); - event.on(EVENT_ID, callback); + synapse.registry.removableListener(PRESS_EVENT, entity.press); } - return entityOut; - } - - return create; + // - Done + return proxy; + }; } diff --git a/src/extensions/registry.extension.ts b/src/extensions/registry.extension.ts index 27c6b2b..58d10b3 100644 --- a/src/extensions/registry.extension.ts +++ b/src/extensions/registry.extension.ts @@ -2,6 +2,7 @@ import { InternalError, is, SECOND, + TBlackHole, TContext, TServiceParams, } from "@digital-alchemy/core"; @@ -57,6 +58,7 @@ export function Registry({ config, internal, context, + event, scheduler, }: TServiceParams) { const LOADERS = new Map object[]>(); @@ -238,10 +240,61 @@ export function Registry({ domains.set(domain, out as unknown as TDomain); return out; } + create.registeredDomains = domains; - return { buildEntityState, create, eventName: name }; + + return { + buildEntityState, + + /** + * Listen for specific socket events, and transfer to internal event bus + */ + busTransfer({ context, eventName, unique_id }: BusTransferOptions) { + const target = `synapse/${eventName}/${unique_id}`; + const source = name(eventName); + hass.socket.onEvent({ + context, + event: source, + exec({ data }: BaseEvent) { + if (data.unique_id !== unique_id) { + return; + } + event.emit(target, data); + }, + }); + logger.debug({ source, target }, `setting up bus transfer`); + return target; + }, + create, + eventName: name, + /** + * Generate an event listener that is easy to remove by developer + */ + removableListener( + eventName: string, + callback: (data: DATA, remove: () => void) => TBlackHole, + ) { + const remove = () => event.removeListener(eventName, exec); + const exec = async (data: DATA) => + await internal.safeExec(async () => await callback(data, remove)); + event.on(eventName, exec); + return { remove }; + }, + }; } +type BusTransferOptions = { + context: TContext; + eventName: string; + unique_id: TSynapseId; +}; + +type BaseEvent = { + data: { + unique_id: TSynapseId; + }; +}; + export type TRegistry = { add(data: DATA, entity: { unique_id?: string }): TSynapseId; byId(unique_id: TSynapseId): DATA; diff --git a/src/extensions/sensor.extension.ts b/src/extensions/sensor.extension.ts index 8f9ebb3..f9aada7 100644 --- a/src/extensions/sensor.extension.ts +++ b/src/extensions/sensor.extension.ts @@ -1,38 +1,33 @@ import { TServiceParams } from "@digital-alchemy/core"; import { - SENSOR_CONFIGURATION_KEYS, + SENSOR_DEVICE_CLASS_CONFIG_KEYS, SensorConfiguration, SensorValue, + SynapseSensorParams, + SynapseVirtualSensor, TRegistry, - TSensor, - TVirtualSensor, + VIRTUAL_ENTITY_BASE_KEYS, } from ".."; export function VirtualSensor({ context, synapse, logger }: TServiceParams) { - const registry = synapse.registry.create({ + const registry = synapse.registry.create({ context, - details: entity => ({ - attributes: entity._rawAttributes, - configuration: entity._rawConfiguration, - state: entity.state, - }), domain: "sensor", }); - // #MARK: create - function create< + return function < STATE extends SensorValue = SensorValue, ATTRIBUTES extends object = object, - CONFIGURATION extends SensorConfiguration = SensorConfiguration, - >(entity: TSensor) { - const entityOut = new Proxy({} as TVirtualSensor, { + >(entity: SynapseSensorParams) { + // - Provide additional defaults + entity.defaultState ??= "" as STATE; + + // - Define the proxy + const proxy = new Proxy({} as SynapseVirtualSensor, { // #MARK: get - get(_, property: keyof TVirtualSensor) { - // * state - if (property === "state") { - return loader.state; - } + get(_, property: keyof SynapseVirtualSensor) { + // > common // * name if (property === "name") { return entity.name; @@ -53,108 +48,76 @@ export function VirtualSensor({ context, synapse, logger }: TServiceParams) { if (property === "_rawAttributes") { return loader.attributes; } + // * attributes + if (property === "attributes") { + return loader.attributesProxy(); + } + // * configuration + if (property === "configuration") { + return loader.configurationProxy(); + } + // > domain specific + // * state + if (property === "state") { + return loader.state; + } // * reset if (property === "reset") { return function () { + logger.debug( + { context: entity.context, name: entity.name }, + `reset`, + ); // what it means to "reset" is up to dev entity.last_reset = new Date(); - logger.debug(`reset`); }; } - // * attributes - if (property === "attributes") { - return new Proxy({} as ATTRIBUTES, { - get: >( - _: ATTRIBUTES, - property: KEY, - ) => { - return loader.attributes[property]; - }, - set: < - KEY extends Extract, - VALUE extends ATTRIBUTES[KEY], - >( - _: ATTRIBUTES, - property: KEY, - value: VALUE, - ) => { - loader.setAttribute(property, value); - return true; - }, - }); - } - // * configuration - if (property === "configuration") { - return new Proxy({} as CONFIGURATION, { - get: >( - _: CONFIGURATION, - property: KEY, - ) => { - return loader.configuration[property]; - }, - set: < - KEY extends Extract, - VALUE extends CONFIGURATION[KEY], - >( - _: CONFIGURATION, - property: KEY, - value: VALUE, - ) => { - loader.setConfiguration(property, value); - return true; - }, - }); - } return undefined; }, - // #MARK: ownKeys - ownKeys: () => { - return [ - "attributes", - "configuration", - "_rawAttributes", - "_rawConfiguration", - "name", - "onUpdate", - "state", - "reset", - ]; - }, + + ownKeys: () => [...VIRTUAL_ENTITY_BASE_KEYS, "reset", "state"], + // #MARK: set set(_, property: string, value: unknown) { - // entity.state = ... - if (property === "state") { - loader.setState(value as STATE); - return true; - } - // entity.attributes = { ... } + // > common + // * attributes if (property === "attributes") { loader.setAttributes(value as ATTRIBUTES); return true; } - // not supported: - // entity.configuration = {...} + // > domain specific + // * state + if (property === "state") { + loader.setState(value as STATE); + return true; + } return false; }, }); - // Validate a good id was passed, and it's the only place in code that's using it - const unique_id = registry.add(entityOut, entity); + // - Add to registry + const unique_id = registry.add(proxy, entity); - const loader = synapse.storage.wrapper({ + // - Initialize value storage + const loader = synapse.storage.wrapper< + STATE, + ATTRIBUTES, + SensorConfiguration + >({ + load_keys: [ + ...SENSOR_DEVICE_CLASS_CONFIG_KEYS, + "last_reset", + "options", + "state_class", + "suggested_display_precision", + "unit_of_measurement", + ] as (keyof SensorConfiguration)[], name: entity.name, registry: registry as TRegistry, unique_id, - value: { - attributes: (entity.defaultAttributes ?? {}) as ATTRIBUTES, - configuration: Object.fromEntries( - SENSOR_CONFIGURATION_KEYS.map(key => [key, entity[key]]), - ) as CONFIGURATION, - state: (entity.defaultState ?? "") as STATE, - }, }); - return entityOut; - } - return create; + // - Done + return proxy; + }; } diff --git a/src/extensions/storage.extension.ts b/src/extensions/storage.extension.ts index 6ca7f28..07e964d 100644 --- a/src/extensions/storage.extension.ts +++ b/src/extensions/storage.extension.ts @@ -4,17 +4,7 @@ import { ENTITY_STATE, PICK_ENTITY } from "@digital-alchemy/hass"; import { BASE_CONFIG_KEYS, BaseEntityParams, TSynapseId } from ".."; import { TRegistry } from "."; -type StorageData = { - attributes?: ATTRIBUTES; - configuration?: CONFIGURATION; - state?: STATE; - last_reported?: string; -}; -type LoaderOptions< - STATE, - ATTRIBUTES extends object, - CONFIGURATION extends object, -> = { +type LoaderOptions = { registry: TRegistry; unique_id: TSynapseId; name: string; @@ -40,7 +30,7 @@ export function ValueStorage({ logger, lifecycle, hass }: TServiceParams) { name, load_keys, config_defaults, - }: LoaderOptions) { + }: LoaderOptions) { const domain = registry.domain; // #MARK: value load diff --git a/src/helpers/domains/button.ts b/src/helpers/domains/button.ts index a6bbbc3..bea14b2 100644 --- a/src/helpers/domains/button.ts +++ b/src/helpers/domains/button.ts @@ -1,58 +1,23 @@ -import { TBlackHole, TContext } from "@digital-alchemy/core"; -import { ButtonDeviceClass, PICK_ENTITY } from "@digital-alchemy/hass"; +import { TBlackHole } from "@digital-alchemy/core"; +import { ButtonDeviceClass } from "@digital-alchemy/hass"; -import { BASE_CONFIG_KEYS, EntityConfigCommon } from "../common-config.helper"; -import { UpdateCallback } from "../event"; +import { BaseEntityParams, BaseVirtualEntity } from "../base-domain.helper"; +import { EntityConfigCommon } from "../common-config.helper"; import { TSynapseId } from "../utility.helper"; -export type TButton = { - context: TContext; - // defaultState?: STATE; - defaultAttributes?: ATTRIBUTES; - name: string; -} & ButtonConfiguration; +export type SynapseButtonParams = BaseEntityParams & ButtonConfiguration; export type ButtonConfiguration = EntityConfigCommon & { press?: (remove: () => void) => TBlackHole; device_class?: `${ButtonDeviceClass}`; }; -export const BUTTON_CONFIGURATION_KEYS = [ - ...BASE_CONFIG_KEYS, - "device_class", - // press should not be included -] as (keyof ButtonConfiguration)[]; - -export type TVirtualButton< - STATE extends void = void, - ATTRIBUTES extends object = object, - CONFIGURATION extends ButtonConfiguration = ButtonConfiguration, - ENTITY_ID extends PICK_ENTITY<"sensor"> = PICK_ENTITY<"sensor">, -> = { - /** - * Do not define attributes that change frequently. - * Create new sensors instead - */ - attributes: ATTRIBUTES; - configuration: CONFIGURATION; +export type SynapseVirtualButton = BaseVirtualEntity< + never, + object, + ButtonConfiguration +> & { onPress: (callback: (remove: () => void) => TBlackHole) => void; - _rawAttributes: ATTRIBUTES; - _rawConfiguration: ATTRIBUTES; - name: string; - /** - * look up the entity id, and proxy update events - */ - onUpdate: UpdateCallback; - /** - * NOT USED WITH BUTTONS - * - * Virtual buttons are stateless - */ - state: STATE; - /** - * Used to uniquely identify this entity in home assistant - */ - unique_id: string; }; export type HassButtonUpdateEvent = { data: { unique_id: TSynapseId } }; diff --git a/src/helpers/domains/sensor.ts b/src/helpers/domains/sensor.ts index 2d56ea2..b2c64eb 100644 --- a/src/helpers/domains/sensor.ts +++ b/src/helpers/domains/sensor.ts @@ -1,6 +1,7 @@ import { TBlackHole, TContext } from "@digital-alchemy/core"; import { PICK_ENTITY } from "@digital-alchemy/hass"; +import { BaseVirtualEntity } from "../base-domain.helper"; import { BASE_CONFIG_KEYS, EntityConfigCommon } from "../common-config.helper"; import { UpdateCallback } from "../event"; @@ -297,7 +298,7 @@ export type SensorDeviceClasses = | AtmosphericPressureSensor | DefaultSensor; -export type TSensor< +export type SynapseSensorParams< STATE extends SensorValue, ATTRIBUTES extends object = object, > = { @@ -341,43 +342,11 @@ export type SensorConfiguration = EntityConfigCommon & export type SensorValue = string | number; -export const SENSOR_CONFIGURATION_KEYS = [ - ...BASE_CONFIG_KEYS, - ...SENSOR_DEVICE_CLASS_CONFIG_KEYS, - "last_reset", - "options", - "state_class", - "suggested_display_precision", - "unit_of_measurement", -] as (keyof SensorConfiguration)[]; - -export type TVirtualSensor< - STATE extends SensorValue = SensorValue, - ATTRIBUTES extends object = object, - CONFIGURATION extends SensorConfiguration = SensorConfiguration, - ENTITY_ID extends PICK_ENTITY<"sensor"> = PICK_ENTITY<"sensor">, -> = { - /** - * Do not define attributes that change frequently. - * Create new sensors instead - */ - attributes: ATTRIBUTES; - configuration: CONFIGURATION; - _rawAttributes: ATTRIBUTES; - _rawConfiguration: ATTRIBUTES; - name: string; - /** - * look up the entity id, and - */ - onUpdate: UpdateCallback; - /** - * the current state - */ - state: STATE; - /** - * Used to uniquely identify this entity in home assistant - */ - unique_id: string; +export type SynapseVirtualSensor = BaseVirtualEntity< + SensorValue, + object, + SensorConfiguration +> & { /** * bumps the last reset time */