From e184e13f100971c1961048ff6557d8d4036f22dd Mon Sep 17 00:00:00 2001 From: Zoe Codez Date: Mon, 15 Apr 2024 15:28:25 -0500 Subject: [PATCH 1/5] roughing out registry --- src/extensions/index.ts | 1 + src/extensions/registry.extension.ts | 141 ++++++++++++++++++++++ src/extensions/websocket-api.extension.ts | 2 +- src/hass.module.ts | 6 + src/helpers/index.ts | 1 + src/helpers/registry.ts | 63 ++++++++++ 6 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 src/extensions/registry.extension.ts create mode 100644 src/helpers/registry.ts diff --git a/src/extensions/index.ts b/src/extensions/index.ts index fb72c1d..1053ca0 100644 --- a/src/extensions/index.ts +++ b/src/extensions/index.ts @@ -2,5 +2,6 @@ export * from "./call-proxy.extension"; export * from "./config.extension"; export * from "./entity-manager.extension"; export * from "./fetch-api.extension"; +export * from "./registry.extension"; export * from "./utilities.extension"; export * from "./websocket-api.extension"; diff --git a/src/extensions/registry.extension.ts b/src/extensions/registry.extension.ts new file mode 100644 index 0000000..2bc4ca3 --- /dev/null +++ b/src/extensions/registry.extension.ts @@ -0,0 +1,141 @@ +import { TServiceParams } from "@digital-alchemy/core"; + +import { + EditAliasOptions, + EditLabelOptions, + EntityRegistryItem, + LabelDefinition, + LabelOptions, + PICK_ENTITY, + ToggleExpose, + UPDATE_REGISTRY, +} from "../helpers"; + +export function Registry({ hass, logger, lifecycle }: TServiceParams) { + async function CreateLabel(options: LabelOptions) { + logger.info({ options }, `create label`); + await hass.socket.sendMessage({ + type: "config/label/create", + ...options, + }); + } + + async function RemoveLabel({ entity, label }: EditLabelOptions) { + const current = await EntityGet(entity); + if (!current?.labels?.includes(label)) { + logger.debug({ name: entity }, `does not have label {%s}`, label); + return; + } + logger.debug({ name: entity }, `removing label [%s]`, label); + await hass.socket.sendMessage({ + entity_id: entity, + labels: current.labels.filter(i => i !== label), + type: UPDATE_REGISTRY, + }); + } + + async function ListLabels() { + return await hass.socket.sendMessage({ + type: "config/label_registry/list", + }); + } + + async function AddLabel({ entity, label }: EditLabelOptions) { + const current = await EntityGet(entity); + if (current?.labels?.includes(label)) { + logger.debug({ name: entity }, `already has label {%s}`, label); + return; + } + await hass.socket.sendMessage({ + entity_id: entity, + labels: [...current.labels, label], + type: UPDATE_REGISTRY, + }); + } + + async function EntityGet(entity_id: ENTITY) { + return await hass.socket.sendMessage>({ + entity_id: entity_id, + type: "config/entity_registry/get", + }); + } + + async function AddAlias({ entity, alias }: EditAliasOptions) { + const current = await EntityGet(entity); + if (current?.aliases?.includes(alias)) { + logger.debug({ name: entity }, `already has alias {%s}`, alias); + return; + } + await hass.socket.sendMessage({ + entity_id: entity, + type: UPDATE_REGISTRY, + }); + } + + async function RemoveAlias({ entity, alias }: EditAliasOptions) { + const current = await EntityGet(entity); + if (!current?.aliases?.includes(alias)) { + logger.debug({ name: entity }, `does not have alias {%s}`, alias); + return; + } + await hass.socket.sendMessage({ + entity_id: entity, + type: UPDATE_REGISTRY, + }); + } + + async function SetConversational({ + entity_ids, + assistants, + should_expose, + }: ToggleExpose) { + await hass.socket.sendMessage({ + assistants: [assistants].flat(), + entity_ids: [entity_ids].flat(), + should_expose, + type: UPDATE_REGISTRY, + }); + } + + async function EntityList() { + await hass.socket.sendMessage({ + type: "config/entity_registry/list_for_display", + }); + } + + async function DeviceList() { + await hass.socket.sendMessage({ + type: "config/device_registry/list", + }); + } + + async function ListAreas() { + await hass.socket.sendMessage({ + type: "config/area_registry/list", + }); + } + + return { + area: { + list: ListAreas, + }, + conversation: { + addAlias: AddAlias, + removeAlias: RemoveAlias, + setConversational: SetConversational, + }, + device: { + list: DeviceList, + }, + entity: { + addLabel: AddLabel, + get: EntityGet, + list: EntityList, + removeLabel: RemoveLabel, + }, + label: { + create: CreateLabel, + list: ListLabels, + }, + }; +} diff --git a/src/extensions/websocket-api.extension.ts b/src/extensions/websocket-api.extension.ts index 938dadb..e71e56f 100644 --- a/src/extensions/websocket-api.extension.ts +++ b/src/extensions/websocket-api.extension.ts @@ -245,7 +245,7 @@ export function WebsocketAPI({ async function sendMessage( data: { - type: `${HASSIO_WS_COMMAND}`; + type: string; id?: number; [key: string]: unknown; }, diff --git a/src/hass.module.ts b/src/hass.module.ts index dff5a49..b105187 100644 --- a/src/hass.module.ts +++ b/src/hass.module.ts @@ -5,6 +5,7 @@ import { Configure, EntityManager, FetchAPI, + Registry, Utilities, WebsocketAPI, } from "./extensions"; @@ -106,6 +107,11 @@ export const LIB_HASS = CreateLibrary({ */ fetch: FetchAPI, + /** + * Interact with the home assistant registry + */ + registry: Registry, + /** * websocket interface */ diff --git a/src/helpers/index.ts b/src/helpers/index.ts index ec23836..93022c2 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -3,5 +3,6 @@ export * from "./constants.helper"; export * from "./entity-state.helper"; export * from "./fetch"; export * from "./metrics.helper"; +export * from "./registry"; export * from "./utility.helper"; export * from "./websocket.helper"; diff --git a/src/helpers/registry.ts b/src/helpers/registry.ts new file mode 100644 index 0000000..bd85ed5 --- /dev/null +++ b/src/helpers/registry.ts @@ -0,0 +1,63 @@ +import { PICK_ENTITY } from "./utility.helper"; + +export type LabelOptions = { + name: string; + icon: string; + color: string; +}; + +export type EditLabelOptions = { + entity: PICK_ENTITY; + label: string; +}; + +export type EditAliasOptions = { + entity: PICK_ENTITY; + alias: string; +}; + +export type LabelDefinition = { + color: string; + description?: string; + icon: string; + label_id: string; + name: string; +}; + +export type ToggleExpose = { + assistants: string | string[]; + entity_ids: PICK_ENTITY | PICK_ENTITY[]; + should_expose: boolean; +}; + +export type EntityRegistryItem = { + area_id?: string; + categories: object; + config_entry_id?: string; + device_id?: string; + disabled_by?: string; + entity_category?: string; + entity_id: ENTITY; + has_entity_name: boolean; + hidden_by?: string; + icon?: string; + id: string; + labels: string[]; + name?: string; + options: { + conversation: { + should_expose?: boolean; + }; + }; + original_name: string; + platform: string; + translation_key?: string; + unique_id: string; + aliases: string[]; + capabilities?: string; + device_class?: string; + original_device_class?: string; + original_icon?: string; +}; + +export const UPDATE_REGISTRY = "config/entity_registry/update"; From 414792b31c2a4247034df5eea12b1de76c7b4675 Mon Sep 17 00:00:00 2001 From: Zoe Codez Date: Mon, 15 Apr 2024 16:23:56 -0500 Subject: [PATCH 2/5] api breakout --- .vscode/settings.json | 5 + cspell.config.yaml | 9 +- src/extensions/area.extension.ts | 39 +++++ src/extensions/conversation.extension.ts | 45 ++++++ src/extensions/device.extension.ts | 11 ++ ...nager.extension.ts => entity.extension.ts} | 58 +++++++ src/extensions/floor.extension.ts | 39 +++++ src/extensions/index.ts | 2 +- src/extensions/label.extension.ts | 39 +++++ src/extensions/registry.extension.ts | 142 +++--------------- src/extensions/zone.extension.ts | 31 ++++ src/hass.module.ts | 15 +- src/helpers/fetch/configuration.ts | 17 +++ src/helpers/registry.ts | 117 ++++++++++++++- 14 files changed, 438 insertions(+), 131 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 src/extensions/area.extension.ts create mode 100644 src/extensions/conversation.extension.ts create mode 100644 src/extensions/device.extension.ts rename src/extensions/{entity-manager.extension.ts => entity.extension.ts} (88%) create mode 100644 src/extensions/floor.extension.ts create mode 100644 src/extensions/label.extension.ts create mode 100644 src/extensions/zone.extension.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..05d19d1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "cSpell.words": [ + "zeroconf" + ] +} diff --git a/cspell.config.yaml b/cspell.config.yaml index e8cb94e..056b962 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -3,9 +3,14 @@ ignorePaths: [] dictionaryDefinitions: [] dictionaries: [] words: + - codeowners + - entites - hass - - quickboot - hassio - - entites + - homekit + - macaddress + - quickboot + - ssdp + - SYSTYPE ignoreWords: [] import: [] diff --git a/src/extensions/area.extension.ts b/src/extensions/area.extension.ts new file mode 100644 index 0000000..6d1c87a --- /dev/null +++ b/src/extensions/area.extension.ts @@ -0,0 +1,39 @@ +import { TServiceParams } from "@digital-alchemy/core"; + +import { AreaCreate, AreaDetails, TAreaId } from "../helpers"; + +export function Area({ hass, lifecycle, config }: TServiceParams) { + lifecycle.onBootstrap(async () => { + if (!config.hass.AUTO_CONNECT_SOCKET || !config.hass.MANAGE_REGISTRY) { + return; + } + hass.floor.current = await hass.floor.list(); + }); + + return { + async create(details: AreaCreate) { + await hass.socket.sendMessage({ + type: "config/area_registry/create", + ...details, + }); + }, + current: [] as AreaDetails[], + async delete(area_id: TAreaId) { + await hass.socket.sendMessage({ + area_id, + type: "config/area_registry/delete", + }); + }, + async list() { + return await hass.socket.sendMessage({ + type: "config/area_registry/list", + }); + }, + async update(details: AreaDetails) { + await hass.socket.sendMessage({ + type: "config/area_registry/update", + ...details, + }); + }, + }; +} diff --git a/src/extensions/conversation.extension.ts b/src/extensions/conversation.extension.ts new file mode 100644 index 0000000..4f1e89e --- /dev/null +++ b/src/extensions/conversation.extension.ts @@ -0,0 +1,45 @@ +import { TServiceParams } from "@digital-alchemy/core"; + +import { EditAliasOptions, ToggleExpose, UPDATE_REGISTRY } from "../helpers"; + +export function Conversation({ hass, logger }: TServiceParams) { + async function AddAlias({ entity, alias }: EditAliasOptions) { + const current = await hass.entity.registry.get(entity); + if (current?.aliases?.includes(alias)) { + logger.debug({ name: entity }, `already has alias {%s}`, alias); + return; + } + await hass.socket.sendMessage({ + entity_id: entity, + type: UPDATE_REGISTRY, + }); + } + + async function RemoveAlias({ entity, alias }: EditAliasOptions) { + const current = await hass.entity.registry.get(entity); + if (!current?.aliases?.includes(alias)) { + logger.debug({ name: entity }, `does not have alias {%s}`, alias); + return; + } + await hass.socket.sendMessage({ entity_id: entity, type: UPDATE_REGISTRY }); + } + + async function SetConversational({ + entity_ids, + assistants, + should_expose, + }: ToggleExpose) { + await hass.socket.sendMessage({ + assistants: [assistants].flat(), + entity_ids: [entity_ids].flat(), + should_expose, + type: UPDATE_REGISTRY, + }); + } + + return { + addAlias: AddAlias, + removeAlias: RemoveAlias, + setConversational: SetConversational, + }; +} diff --git a/src/extensions/device.extension.ts b/src/extensions/device.extension.ts new file mode 100644 index 0000000..b6795a7 --- /dev/null +++ b/src/extensions/device.extension.ts @@ -0,0 +1,11 @@ +import { TServiceParams } from "@digital-alchemy/core"; + +export function Device({ hass }: TServiceParams) { + return { + async list() { + await hass.socket.sendMessage({ + type: "config/device_registry/list", + }); + }, + }; +} diff --git a/src/extensions/entity-manager.extension.ts b/src/extensions/entity.extension.ts similarity index 88% rename from src/extensions/entity-manager.extension.ts rename to src/extensions/entity.extension.ts index 3259f80..6fd546c 100644 --- a/src/extensions/entity-manager.extension.ts +++ b/src/extensions/entity.extension.ts @@ -16,11 +16,14 @@ import { Get } from "type-fest"; import { ALL_DOMAINS, + EditLabelOptions, ENTITY_STATE, EntityHistoryDTO, EntityHistoryResult, + EntityRegistryItem, HASSIO_WS_COMMAND, PICK_ENTITY, + UPDATE_REGISTRY, } from ".."; type EntityHistoryItem = { a: object; s: unknown; lu: number }; @@ -357,11 +360,58 @@ export function EntityManager({ await refresh(); }); + async function AddLabel({ entity, label }: EditLabelOptions) { + const current = await EntityGet(entity); + if (current?.labels?.includes(label)) { + logger.debug({ name: entity }, `already has label {%s}`, label); + return; + } + await hass.socket.sendMessage({ + entity_id: entity, + labels: [...current.labels, label], + type: UPDATE_REGISTRY, + }); + } + + async function EntitySource() { + return await hass.socket.sendMessage< + Record + >({ type: "entity/source" }); + } + + async function EntityList() { + await hass.socket.sendMessage({ + type: "config/entity_registry/list_for_display", + }); + } + + async function RemoveLabel({ entity, label }: EditLabelOptions) { + const current = await EntityGet(entity); + if (!current?.labels?.includes(label)) { + logger.debug({ name: entity }, `does not have label {%s}`, label); + return; + } + logger.debug({ name: entity }, `removing label [%s]`, label); + await hass.socket.sendMessage({ + entity_id: entity, + labels: current.labels.filter(i => i !== label), + type: UPDATE_REGISTRY, + }); + } + + async function EntityGet(entity_id: ENTITY) { + return await hass.socket.sendMessage>({ + entity_id: entity_id, + type: "config/entity_registry/get", + }); + } + return { /** * Internal library use only */ [ENTITY_UPDATE_RECEIVER]: EntityUpdateReceiver, + /** * Retrieves a proxy object for a specified entity. This proxy object * provides current values and event hooks for the entity. @@ -397,6 +447,14 @@ export function EntityManager({ * synchronization with the latest state data from Home Assistant. */ refresh, + + registry: { + addLabel: AddLabel, + get: EntityGet, + list: EntityList, + removeLabel: RemoveLabel, + source: EntitySource, + }, }; } diff --git a/src/extensions/floor.extension.ts b/src/extensions/floor.extension.ts new file mode 100644 index 0000000..4fa3262 --- /dev/null +++ b/src/extensions/floor.extension.ts @@ -0,0 +1,39 @@ +import { TServiceParams } from "@digital-alchemy/core"; + +import { FloorCreate, FloorDetails, TFloorId } from "../helpers"; + +export function Floor({ hass, lifecycle, config }: TServiceParams) { + lifecycle.onBootstrap(async () => { + if (!config.hass.AUTO_CONNECT_SOCKET || !config.hass.MANAGE_REGISTRY) { + return; + } + hass.floor.current = await hass.floor.list(); + }); + + return { + async create(details: FloorCreate) { + await hass.socket.sendMessage({ + type: "config/floor_registry/create", + ...details, + }); + }, + current: [] as FloorDetails[], + async delete(floor_id: TFloorId) { + await hass.socket.sendMessage({ + floor_id, + type: "config/floor_registry/delete", + }); + }, + async list() { + return await hass.socket.sendMessage({ + type: "config/floor_registry/list", + }); + }, + async update(details: FloorDetails) { + await hass.socket.sendMessage({ + type: "config/floor_registry/update", + ...details, + }); + }, + }; +} diff --git a/src/extensions/index.ts b/src/extensions/index.ts index 1053ca0..11117f1 100644 --- a/src/extensions/index.ts +++ b/src/extensions/index.ts @@ -1,6 +1,6 @@ export * from "./call-proxy.extension"; export * from "./config.extension"; -export * from "./entity-manager.extension"; +export * from "./entity.extension"; export * from "./fetch-api.extension"; export * from "./registry.extension"; export * from "./utilities.extension"; diff --git a/src/extensions/label.extension.ts b/src/extensions/label.extension.ts new file mode 100644 index 0000000..c98e427 --- /dev/null +++ b/src/extensions/label.extension.ts @@ -0,0 +1,39 @@ +import { TServiceParams } from "@digital-alchemy/core"; + +import { LabelDefinition, LabelOptions, TLabelId } from "../helpers"; + +export function Label({ hass, lifecycle, config }: TServiceParams) { + lifecycle.onBootstrap(async () => { + if (!config.hass.AUTO_CONNECT_SOCKET || !config.hass.MANAGE_REGISTRY) { + return; + } + hass.floor.current = await hass.floor.list(); + }); + + return { + async create(details: LabelOptions) { + await hass.socket.sendMessage({ + type: "config/label_registry/create", + ...details, + }); + }, + current: [] as LabelDefinition[], + async delete(area_id: TLabelId) { + await hass.socket.sendMessage({ + area_id, + type: "config/label_registry/delete", + }); + }, + async list() { + return await hass.socket.sendMessage({ + type: "config/label_registry/list", + }); + }, + async update(details: LabelDefinition) { + await hass.socket.sendMessage({ + type: "config/label_registry/update", + ...details, + }); + }, + }; +} diff --git a/src/extensions/registry.extension.ts b/src/extensions/registry.extension.ts index 2bc4ca3..77eb1c9 100644 --- a/src/extensions/registry.extension.ts +++ b/src/extensions/registry.extension.ts @@ -1,141 +1,35 @@ import { TServiceParams } from "@digital-alchemy/core"; import { - EditAliasOptions, - EditLabelOptions, - EntityRegistryItem, - LabelDefinition, - LabelOptions, - PICK_ENTITY, - ToggleExpose, - UPDATE_REGISTRY, + HassConfig, + ManifestItem, + UpdateCoreOptions, + ZoneDetails, } from "../helpers"; -export function Registry({ hass, logger, lifecycle }: TServiceParams) { - async function CreateLabel(options: LabelOptions) { - logger.info({ options }, `create label`); - await hass.socket.sendMessage({ - type: "config/label/create", - ...options, - }); - } - - async function RemoveLabel({ entity, label }: EditLabelOptions) { - const current = await EntityGet(entity); - if (!current?.labels?.includes(label)) { - logger.debug({ name: entity }, `does not have label {%s}`, label); - return; - } - logger.debug({ name: entity }, `removing label [%s]`, label); - await hass.socket.sendMessage({ - entity_id: entity, - labels: current.labels.filter(i => i !== label), - type: UPDATE_REGISTRY, - }); - } - - async function ListLabels() { - return await hass.socket.sendMessage({ - type: "config/label_registry/list", - }); - } - - async function AddLabel({ entity, label }: EditLabelOptions) { - const current = await EntityGet(entity); - if (current?.labels?.includes(label)) { - logger.debug({ name: entity }, `already has label {%s}`, label); - return; - } - await hass.socket.sendMessage({ - entity_id: entity, - labels: [...current.labels, label], - type: UPDATE_REGISTRY, - }); - } - - async function EntityGet(entity_id: ENTITY) { - return await hass.socket.sendMessage>({ - entity_id: entity_id, - type: "config/entity_registry/get", +export function Registry({ hass }: TServiceParams) { + async function ManifestList() { + return await hass.socket.sendMessage({ + type: "manifest/list", }); } - async function AddAlias({ entity, alias }: EditAliasOptions) { - const current = await EntityGet(entity); - if (current?.aliases?.includes(alias)) { - logger.debug({ name: entity }, `already has alias {%s}`, alias); - return; - } - await hass.socket.sendMessage({ - entity_id: entity, - type: UPDATE_REGISTRY, - }); - } - - async function RemoveAlias({ entity, alias }: EditAliasOptions) { - const current = await EntityGet(entity); - if (!current?.aliases?.includes(alias)) { - logger.debug({ name: entity }, `does not have alias {%s}`, alias); - return; - } - await hass.socket.sendMessage({ - entity_id: entity, - type: UPDATE_REGISTRY, - }); - } - - async function SetConversational({ - entity_ids, - assistants, - should_expose, - }: ToggleExpose) { - await hass.socket.sendMessage({ - assistants: [assistants].flat(), - entity_ids: [entity_ids].flat(), - should_expose, - type: UPDATE_REGISTRY, - }); - } - - async function EntityList() { - await hass.socket.sendMessage({ - type: "config/entity_registry/list_for_display", - }); - } - - async function DeviceList() { - await hass.socket.sendMessage({ - type: "config/device_registry/list", + async function UpdateCore(options: UpdateCoreOptions) { + await hass.socket.sendMessage({ + ...options, + type: "config/core/update", }); } - async function ListAreas() { - await hass.socket.sendMessage({ - type: "config/area_registry/list", + async function GetConfig() { + return await hass.socket.sendMessage({ + type: "get_config", }); } return { - area: { - list: ListAreas, - }, - conversation: { - addAlias: AddAlias, - removeAlias: RemoveAlias, - setConversational: SetConversational, - }, - device: { - list: DeviceList, - }, - entity: { - addLabel: AddLabel, - get: EntityGet, - list: EntityList, - removeLabel: RemoveLabel, - }, - label: { - create: CreateLabel, - list: ListLabels, - }, + getConfig: GetConfig, + manifestList: ManifestList, + updateCore: UpdateCore, }; } diff --git a/src/extensions/zone.extension.ts b/src/extensions/zone.extension.ts new file mode 100644 index 0000000..9bef4a8 --- /dev/null +++ b/src/extensions/zone.extension.ts @@ -0,0 +1,31 @@ +import { TServiceParams } from "@digital-alchemy/core"; + +import { ManifestItem, ZoneDetails, ZoneOptions } from "../helpers"; + +export function Zone({ hass }: TServiceParams) { + async function ZoneCreate(options: ZoneOptions) { + await hass.socket.sendMessage({ + ...options, + type: "zone/create", + }); + } + + async function ZoneUpdate(zone_id: string, options: ZoneOptions) { + await hass.socket.sendMessage({ + ...options, + type: "zone/create", + zone_id, + }); + } + + async function ZoneList() { + await hass.socket.sendMessage({ + type: "manifest/list", + }); + } + return { + create: ZoneCreate, + list: ZoneList, + update: ZoneUpdate, + }; +} diff --git a/src/hass.module.ts b/src/hass.module.ts index b105187..54769a8 100644 --- a/src/hass.module.ts +++ b/src/hass.module.ts @@ -9,6 +9,10 @@ import { Utilities, WebsocketAPI, } from "./extensions"; +import { Area } from "./extensions/area.extension"; +import { Device } from "./extensions/device.extension"; +import { Floor } from "./extensions/floor.extension"; +import { Label } from "./extensions/label.extension"; type AllowRestOptions = "prefer" | "allow" | "forbid"; @@ -45,6 +49,11 @@ export const LIB_HASS = CreateLibrary({ "If sendMessage was set to expect a response, a warning will be emitted after this delay if one is not received", type: "number", }, + MANAGE_REGISTRY: { + default: true, + description: "Live track registry data", + type: "boolean", + }, RETRY_INTERVAL: { default: 5, description: "How often to retry connecting on connection failure (ms).", @@ -87,6 +96,7 @@ export const LIB_HASS = CreateLibrary({ // no internal dependency ones first priorityInit: ["fetch", "utils"], services: { + area: Area, /** * general service calling interface */ @@ -97,15 +107,18 @@ export const LIB_HASS = CreateLibrary({ */ configure: Configure, + device: Device, + /** * retrieve and interact with home assistant entities */ entity: EntityManager, - /** * rest api commands */ fetch: FetchAPI, + floor: Floor, + label: Label, /** * Interact with the home assistant registry diff --git a/src/helpers/fetch/configuration.ts b/src/helpers/fetch/configuration.ts index 8e12fa4..e923cd9 100644 --- a/src/helpers/fetch/configuration.ts +++ b/src/helpers/fetch/configuration.ts @@ -1,3 +1,5 @@ +import { TFloorId, TLabelId } from "../registry"; + export interface HassUnitSystem { length: "mi"; mass: "lb"; @@ -36,3 +38,18 @@ export type CheckConfigResult = errors: string; result: "invalid"; }; + +export type AreaDetails = AreaCreate & { + area_id: TAreaId; +}; + +export type AreaCreate = { + floor_id: TFloorId; + aliases?: string[]; + icon: string; + labels: TLabelId[]; + name: string; + picture: string; +}; + +export type TAreaId = string & { area: true }; diff --git a/src/helpers/registry.ts b/src/helpers/registry.ts index bd85ed5..5090f71 100644 --- a/src/helpers/registry.ts +++ b/src/helpers/registry.ts @@ -8,7 +8,7 @@ export type LabelOptions = { export type EditLabelOptions = { entity: PICK_ENTITY; - label: string; + label: TLabelId; }; export type EditAliasOptions = { @@ -16,11 +16,13 @@ export type EditAliasOptions = { alias: string; }; +export type TLabelId = string & { label: string }; + export type LabelDefinition = { color: string; description?: string; icon: string; - label_id: string; + label_id: TLabelId; name: string; }; @@ -42,7 +44,7 @@ export type EntityRegistryItem = { hidden_by?: string; icon?: string; id: string; - labels: string[]; + labels: TLabelId[]; name?: string; options: { conversation: { @@ -61,3 +63,112 @@ export type EntityRegistryItem = { }; export const UPDATE_REGISTRY = "config/entity_registry/update"; + +export type UpdateCoreOptions = { + currency: string; + elevation: number; + unit_system: string; + update_units: boolean; + time_zone: string; + location_name: string; + language: string; + country: string; +}; + +export type ZoneOptions = { + latitude: number; + longitude: number; + name: string; + icon: string; + passive: boolean; +}; +export type ZoneDetails = ZoneOptions & { id: string }; + +export interface ManifestItem { + domain: string; + name: string; + codeowners: string[]; + config_flow?: boolean; + documentation?: string; + iot_class?: IotClass; + loggers?: string[]; + requirements?: string[]; + is_built_in: boolean; + integration_type?: IntegrationType; + quality_scale?: QualityScale; + dependencies?: string[]; + dhcp?: DHCP[]; + after_dependencies?: string[]; + single_config_entry?: boolean; + bluetooth?: Bluetooth[]; + version?: string; + homekit?: Homekit; + zeroconf?: Array; + ssdp?: SSDP[]; + issue_tracker?: string; +} + +export interface Bluetooth { + manufacturer_id: number; + service_uuid?: string; + manufacturer_data_start?: number[]; +} + +export interface DHCP { + registered_devices?: boolean; + hostname?: string; + macaddress?: string; +} + +export interface Homekit { + models: string[]; +} + +export enum IntegrationType { + Device = "device", + Entity = "entity", + Helper = "helper", + Hub = "hub", + Service = "service", + System = "system", +} + +export enum IotClass { + Calculated = "calculated", + CloudPolling = "cloud_polling", + CloudPush = "cloud_push", + LocalPolling = "local_polling", + LocalPush = "local_push", +} + +export enum QualityScale { + Internal = "internal", + Platinum = "platinum", +} + +export interface SSDP { + manufacturer: string; + modelDescription: string; +} + +export interface ZeroconfClass { + type: string; + properties?: Properties; +} + +export interface Properties { + mdl?: string; + SYSTYPE?: string; +} + +export type FloorCreate = { + name: string; + level: number; + aliases: string[]; +}; + +export type TFloorId = string & { floor: boolean }; + +export type FloorDetails = FloorCreate & { + floor_id: TFloorId; +}; From 7fb4c6f93c2ddf970ac3a3c61c0f0bc79d6c56f4 Mon Sep 17 00:00:00 2001 From: Zoe Codez Date: Mon, 15 Apr 2024 17:38:48 -0500 Subject: [PATCH 3/5] workflows --- cspell.config.yaml | 1 + src/extensions/area.extension.ts | 14 ++- ...ities.extension.ts => backup.extension.ts} | 17 ++-- src/extensions/conversation.extension.ts | 12 +-- src/extensions/device.extension.ts | 46 ++++++++-- src/extensions/entity.extension.ts | 89 +++++++++++++++---- src/extensions/floor.extension.ts | 27 +++++- src/extensions/index.ts | 7 +- src/extensions/label.extension.ts | 29 +++++- src/extensions/registry.extension.ts | 8 ++ src/extensions/websocket-api.extension.ts | 33 +++++-- src/extensions/zone.extension.ts | 39 +++++++- src/hass.module.ts | 23 ++--- src/helpers/device.helper.ts | 24 +++++ src/helpers/entity-state.helper.ts | 47 ++++++++++ src/helpers/fetch/configuration.ts | 22 +++++ src/helpers/index.ts | 1 + src/helpers/manifest.helper.ts | 0 src/helpers/registry.ts | 4 +- src/helpers/websocket.helper.ts | 68 +++----------- 20 files changed, 383 insertions(+), 128 deletions(-) rename src/extensions/{utilities.extension.ts => backup.extension.ts} (73%) create mode 100644 src/helpers/device.helper.ts create mode 100644 src/helpers/manifest.helper.ts diff --git a/cspell.config.yaml b/cspell.config.yaml index 056b962..9e6b02e 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -8,6 +8,7 @@ words: - hass - hassio - homekit + - endregion - macaddress - quickboot - ssdp diff --git a/src/extensions/area.extension.ts b/src/extensions/area.extension.ts index 6d1c87a..0d50138 100644 --- a/src/extensions/area.extension.ts +++ b/src/extensions/area.extension.ts @@ -2,12 +2,20 @@ import { TServiceParams } from "@digital-alchemy/core"; import { AreaCreate, AreaDetails, TAreaId } from "../helpers"; -export function Area({ hass, lifecycle, config }: TServiceParams) { - lifecycle.onBootstrap(async () => { +export function Area({ hass, context, config, logger }: TServiceParams) { + hass.socket.onConnect(async () => { if (!config.hass.AUTO_CONNECT_SOCKET || !config.hass.MANAGE_REGISTRY) { return; } - hass.floor.current = await hass.floor.list(); + hass.area.current = await hass.area.list(); + hass.socket.subscribe({ + context, + event_type: "area_registry_updated", + async exec() { + hass.area.current = await hass.area.list(); + logger.debug(`area registry updated`); + }, + }); }); return { diff --git a/src/extensions/utilities.extension.ts b/src/extensions/backup.extension.ts similarity index 73% rename from src/extensions/utilities.extension.ts rename to src/extensions/backup.extension.ts index 6e50174..05c3305 100644 --- a/src/extensions/utilities.extension.ts +++ b/src/extensions/backup.extension.ts @@ -1,8 +1,8 @@ import { HALF, SECOND, sleep, TServiceParams } from "@digital-alchemy/core"; -import { BackupResponse, HASSIO_WS_COMMAND, HomeAssistantBackup } from ".."; +import { BackupResponse, HomeAssistantBackup } from "../helpers"; -export function Utilities({ logger, hass }: TServiceParams) { +export function Backup({ logger, hass }: TServiceParams) { async function generate(): Promise { let current = await list(); // const originalLength = current.backups.length; @@ -13,9 +13,7 @@ export function Utilities({ logger, hass }: TServiceParams) { ); } else { logger.info({ name: generate }, "initiating new backup"); - hass.socket.sendMessage({ - type: HASSIO_WS_COMMAND.generate_backup, - }); + hass.socket.sendMessage({ type: "backup/generate" }); while (current.backing_up === false) { logger.debug({ name: generate }, "... waiting"); await sleep(HALF * SECOND); @@ -34,17 +32,14 @@ export function Utilities({ logger, hass }: TServiceParams) { async function list(): Promise { logger.trace({ name: list }, "send"); return await hass.socket.sendMessage({ - type: HASSIO_WS_COMMAND.backup_info, + type: "backup/info", }); } async function remove(slug: string): Promise { logger.trace({ name: remove }, "send"); - await hass.socket.sendMessage( - { slug, type: HASSIO_WS_COMMAND.remove_backup }, - false, - ); + await hass.socket.sendMessage({ slug, type: "backup/remove" }, false); } - return { backup: { generate, list, remove } }; + return { generate, list, remove }; } diff --git a/src/extensions/conversation.extension.ts b/src/extensions/conversation.extension.ts index 4f1e89e..11540f6 100644 --- a/src/extensions/conversation.extension.ts +++ b/src/extensions/conversation.extension.ts @@ -3,7 +3,7 @@ import { TServiceParams } from "@digital-alchemy/core"; import { EditAliasOptions, ToggleExpose, UPDATE_REGISTRY } from "../helpers"; export function Conversation({ hass, logger }: TServiceParams) { - async function AddAlias({ entity, alias }: EditAliasOptions) { + async function addAlias({ entity, alias }: EditAliasOptions) { const current = await hass.entity.registry.get(entity); if (current?.aliases?.includes(alias)) { logger.debug({ name: entity }, `already has alias {%s}`, alias); @@ -15,7 +15,7 @@ export function Conversation({ hass, logger }: TServiceParams) { }); } - async function RemoveAlias({ entity, alias }: EditAliasOptions) { + async function removeAlias({ entity, alias }: EditAliasOptions) { const current = await hass.entity.registry.get(entity); if (!current?.aliases?.includes(alias)) { logger.debug({ name: entity }, `does not have alias {%s}`, alias); @@ -24,7 +24,7 @@ export function Conversation({ hass, logger }: TServiceParams) { await hass.socket.sendMessage({ entity_id: entity, type: UPDATE_REGISTRY }); } - async function SetConversational({ + async function setConversational({ entity_ids, assistants, should_expose, @@ -38,8 +38,8 @@ export function Conversation({ hass, logger }: TServiceParams) { } return { - addAlias: AddAlias, - removeAlias: RemoveAlias, - setConversational: SetConversational, + addAlias, + removeAlias, + setConversational, }; } diff --git a/src/extensions/device.extension.ts b/src/extensions/device.extension.ts index b6795a7..505d3a7 100644 --- a/src/extensions/device.extension.ts +++ b/src/extensions/device.extension.ts @@ -1,11 +1,45 @@ import { TServiceParams } from "@digital-alchemy/core"; -export function Device({ hass }: TServiceParams) { +import { DeviceDetails } from "../helpers"; + +export function Device({ + hass, + config, + lifecycle, + context, + logger, +}: TServiceParams) { + lifecycle.onBootstrap(async () => { + if (!config.hass.AUTO_CONNECT_SOCKET || !config.hass.MANAGE_REGISTRY) { + return; + } + hass.device.current = await hass.device.list(); + hass.socket.subscribe({ + context, + event_type: "device_registry_updated", + async exec() { + hass.device.current = await hass.device.list(); + logger.debug(`device registry updated`); + }, + }); + await SubscribeUpdates(); + }); + + async function SubscribeUpdates() { + await hass.socket.sendMessage({ + event_type: "device_registry_updated", + type: "subscribe_events", + }); + } + + async function list() { + return await hass.socket.sendMessage({ + type: "config/device_registry/list", + }); + } + return { - async list() { - await hass.socket.sendMessage({ - type: "config/device_registry/list", - }); - }, + current: [] as DeviceDetails[], + list, }; } diff --git a/src/extensions/entity.extension.ts b/src/extensions/entity.extension.ts index 6fd546c..2f0c5f8 100644 --- a/src/extensions/entity.extension.ts +++ b/src/extensions/entity.extension.ts @@ -18,11 +18,16 @@ import { ALL_DOMAINS, EditLabelOptions, ENTITY_STATE, + EntityDetails, EntityHistoryDTO, EntityHistoryResult, EntityRegistryItem, HASSIO_WS_COMMAND, PICK_ENTITY, + TAreaId, + TDeviceId, + TFloorId, + TLabelId, UPDATE_REGISTRY, } from ".."; @@ -67,10 +72,12 @@ const RECENT = 5; export function EntityManager({ logger, hass, + config, lifecycle, + context, internal, }: TServiceParams) { - // # Local vars + // #MARK: Local vars /** * MASTER_STATE.switch.desk_light = {entity_id,state,attributes,...} */ @@ -87,8 +94,7 @@ export function EntityManager({ event.setMaxListeners(UNLIMITED); let init = false; - // # Methods - // ## Retrieve raw state object for entity + // #MARK: getCurrentState function getCurrentState( entity_id: ENTITY_ID, // 🖕 TS @@ -97,7 +103,7 @@ export function EntityManager({ return out as ENTITY_STATE; } - // ## Proxy version of the logic + // #MARK:proxyGetLogic function proxyGetLogic< ENTITY extends PICK_ENTITY = PICK_ENTITY, PROPERTY extends string = string, @@ -129,7 +135,7 @@ export function EntityManager({ return internal.utils.object.get(current, property) || defaultValue; } - // ## Retrieve a proxy by id + // #MARK: byId function byId( entity_id: ENTITY_ID, ): ByIdProxy { @@ -205,7 +211,7 @@ export function EntityManager({ return ENTITY_PROXIES.get(entity_id) as ByIdProxy; } - // ## Retrieve entity history (via socket) + // #MARK: history async function history( payload: Omit, "type">, ) { @@ -234,7 +240,7 @@ export function EntityManager({ ); } - // ## Build a string array of all known entity ids + // #MARK: listEntities function listEntities(): PICK_ENTITY[] { return Object.keys(MASTER_STATE).flatMap(domain => Object.keys(MASTER_STATE[domain as ALL_DOMAINS]).map( @@ -243,14 +249,14 @@ export function EntityManager({ ); } - // ## Gather all entity proxies for a domain + // #MARK: findByDomain function findByDomain(domain: DOMAIN) { return Object.keys(MASTER_STATE[domain] ?? {}).map(i => byId(`${domain}.${i}` as PICK_ENTITY), ); } - // ## Load all entity state information from hass + // #MARK: refresh async function refresh(recursion = START): Promise { const now = dayjs(); if (lastRefresh) { @@ -328,12 +334,12 @@ export function EntityManager({ init = true; } - // ## is.entity definition + // #MARK: is.entity // Actually tie the type casting to real state is.entity = (entityId: PICK_ENTITY): entityId is PICK_ENTITY => is.undefined(internal.utils.object.get(MASTER_STATE, entityId)); - // ## Receiver function for incoming entity updates + // #MARK: EntityUpdateReceiver function EntityUpdateReceiver( entity_id: PICK_ENTITY, new_state: ENTITY_STATE, @@ -360,6 +366,7 @@ export function EntityManager({ await refresh(); }); + // #region Registry async function AddLabel({ entity, label }: EditLabelOptions) { const current = await EntityGet(entity); if (current?.labels?.includes(label)) { @@ -380,7 +387,7 @@ export function EntityManager({ } async function EntityList() { - await hass.socket.sendMessage({ + return await hass.socket.sendMessage({ type: "config/entity_registry/list_for_display", }); } @@ -406,17 +413,65 @@ export function EntityManager({ }); } + lifecycle.onBootstrap(async () => { + if (!config.hass.AUTO_CONNECT_SOCKET || !config.hass.MANAGE_REGISTRY) { + return; + } + hass.entity.registry.current = await hass.entity.registry.list(); + hass.socket.subscribe({ + context, + event_type: "entity_registry_updated", + async exec() { + logger.debug("entity registry updated"); + hass.entity.registry.current = await hass.entity.registry.list(); + }, + }); + }); + + // #endregion + function byLabel(label: TLabelId): PICK_ENTITY[] { + return hass.entity.registry.current + .filter(i => i.labels.includes(label)) + .map(i => i.entity_id); + } + + function byArea(area: TAreaId): PICK_ENTITY[] { + return hass.entity.registry.current + .filter(i => i.area_id === area) + .map(i => i.entity_id); + } + + function byDevice(device: TDeviceId): PICK_ENTITY[] { + return hass.entity.registry.current + .filter(i => i.device_id === device) + .map(i => i.entity_id); + } + + function byFloor(floor: TFloorId): PICK_ENTITY[] { + const areas = new Set( + hass.area.current.filter(i => i.floor_id === floor).map(i => i.area_id), + ); + return hass.entity.registry.current + .filter(i => areas.has(i.area_id)) + .map(i => i.entity_id); + } + + // #MARK: return object return { /** * Internal library use only */ [ENTITY_UPDATE_RECEIVER]: EntityUpdateReceiver, + byArea, + byDevice, + + byFloor, /** * Retrieves a proxy object for a specified entity. This proxy object * provides current values and event hooks for the entity. - */ - byId, + */ byId, + byLabel, /** * Lists all entities within a specified domain. This is useful for @@ -448,8 +503,12 @@ export function EntityManager({ */ refresh, + /** + * Interact with the entity registry + */ registry: { addLabel: AddLabel, + current: [] as EntityDetails[], get: EntityGet, list: EntityList, removeLabel: RemoveLabel, @@ -460,6 +519,6 @@ export function EntityManager({ declare module "@digital-alchemy/core" { export interface IsIt { - entity(entity: PICK_ENTITY): entity is PICK_ENTITY; + entity(entity: string): entity is PICK_ENTITY; } } diff --git a/src/extensions/floor.extension.ts b/src/extensions/floor.extension.ts index 4fa3262..b8e18e2 100644 --- a/src/extensions/floor.extension.ts +++ b/src/extensions/floor.extension.ts @@ -1,15 +1,32 @@ -import { TServiceParams } from "@digital-alchemy/core"; +import { is, TServiceParams } from "@digital-alchemy/core"; import { FloorCreate, FloorDetails, TFloorId } from "../helpers"; -export function Floor({ hass, lifecycle, config }: TServiceParams) { +export function Floor({ + hass, + lifecycle, + config, + context, + logger, +}: TServiceParams) { lifecycle.onBootstrap(async () => { if (!config.hass.AUTO_CONNECT_SOCKET || !config.hass.MANAGE_REGISTRY) { return; } hass.floor.current = await hass.floor.list(); + hass.socket.subscribe({ + context, + event_type: "floor_registry_updated", + async exec() { + hass.floor.current = await hass.floor.list(); + logger.debug(`floor registry updated`); + }, + }); }); + is.floor = (floor: string): floor is TFloorId => + hass.floor.current.some(i => i.floor_id === floor); + return { async create(details: FloorCreate) { await hass.socket.sendMessage({ @@ -37,3 +54,9 @@ export function Floor({ hass, lifecycle, config }: TServiceParams) { }, }; } + +declare module "@digital-alchemy/core" { + export interface IsIt { + floor(floor: string): floor is TFloorId; + } +} diff --git a/src/extensions/index.ts b/src/extensions/index.ts index 11117f1..d8a499c 100644 --- a/src/extensions/index.ts +++ b/src/extensions/index.ts @@ -1,7 +1,12 @@ +export * from "./area.extension"; +export * from "./backup.extension"; export * from "./call-proxy.extension"; export * from "./config.extension"; +export * from "./device.extension"; export * from "./entity.extension"; export * from "./fetch-api.extension"; +export * from "./floor.extension"; +export * from "./label.extension"; export * from "./registry.extension"; -export * from "./utilities.extension"; export * from "./websocket-api.extension"; +export * from "./zone.extension"; diff --git a/src/extensions/label.extension.ts b/src/extensions/label.extension.ts index c98e427..fc1d7f5 100644 --- a/src/extensions/label.extension.ts +++ b/src/extensions/label.extension.ts @@ -1,15 +1,32 @@ -import { TServiceParams } from "@digital-alchemy/core"; +import { is, TServiceParams } from "@digital-alchemy/core"; import { LabelDefinition, LabelOptions, TLabelId } from "../helpers"; -export function Label({ hass, lifecycle, config }: TServiceParams) { +export function Label({ + hass, + lifecycle, + config, + logger, + context, +}: TServiceParams) { lifecycle.onBootstrap(async () => { if (!config.hass.AUTO_CONNECT_SOCKET || !config.hass.MANAGE_REGISTRY) { return; } - hass.floor.current = await hass.floor.list(); + hass.label.current = await hass.label.list(); + hass.socket.subscribe({ + context, + event_type: "label_registry_updated", + async exec() { + hass.label.current = await hass.label.list(); + logger.debug(`label registry updated`); + }, + }); }); + is.label = (label: TLabelId): label is TLabelId => + hass.label.current.some(i => i.label_id === label); + return { async create(details: LabelOptions) { await hass.socket.sendMessage({ @@ -37,3 +54,9 @@ export function Label({ hass, lifecycle, config }: TServiceParams) { }, }; } + +declare module "@digital-alchemy/core" { + export interface IsIt { + label(label: string): label is TLabelId; + } +} diff --git a/src/extensions/registry.extension.ts b/src/extensions/registry.extension.ts index 77eb1c9..5dc7861 100644 --- a/src/extensions/registry.extension.ts +++ b/src/extensions/registry.extension.ts @@ -1,6 +1,7 @@ import { TServiceParams } from "@digital-alchemy/core"; import { + ConfigEntry, HassConfig, ManifestItem, UpdateCoreOptions, @@ -27,8 +28,15 @@ export function Registry({ hass }: TServiceParams) { }); } + async function GetConfigEntries() { + return await hass.socket.sendMessage({ + type: "config_entries/get", + }); + } + return { getConfig: GetConfig, + getConfigEntries: GetConfigEntries, manifestList: ManifestList, updateCore: UpdateCore, }; diff --git a/src/extensions/websocket-api.extension.ts b/src/extensions/websocket-api.extension.ts index e71e56f..7bc4060 100644 --- a/src/extensions/websocket-api.extension.ts +++ b/src/extensions/websocket-api.extension.ts @@ -15,7 +15,6 @@ import WS from "ws"; import { ENTITY_UPDATE_RECEIVER, EntityUpdateEvent, - HASSIO_WS_COMMAND, HassSocketMessageTypes, OnHassEventOptions, SOCKET_CONNECTION_STATE, @@ -25,6 +24,7 @@ import { SOCKET_RECEIVED_MESSAGES, SOCKET_SENT_MESSAGES, SocketMessageDTO, + SocketSubscribeOptions, } from ".."; let connection: WS; @@ -118,7 +118,7 @@ export function WebsocketAPI({ lastPingAttempt = now; // emit a ping, do not wait for reply (inline) - sendMessage({ type: HASSIO_WS_COMMAND.ping }, false); + sendMessage({ type: "ping" }, false); // reply will be captured by this, waiting at most a second pingSleep = sleep(SECOND); @@ -260,12 +260,12 @@ export function WebsocketAPI({ return undefined; } - if (hass.socket.pauseMessages && data.type !== HASSIO_WS_COMMAND.ping) { + if (hass.socket.pauseMessages && data.type !== "ping") { return undefined; } countMessage(data.type); const id = messageCount; - if (data.type !== HASSIO_WS_COMMAND.auth) { + if (data.type !== "auth") { data.id = id; } const json = JSON.stringify(data); @@ -415,16 +415,13 @@ export function WebsocketAPI({ switch (message.type as HassSocketMessageTypes) { case HassSocketMessageTypes.auth_required: logger.trace({ name: onMessage }, `sending authentication`); - sendMessage( - { access_token: config.hass.TOKEN, type: HASSIO_WS_COMMAND.auth }, - false, - ); + sendMessage({ access_token: config.hass.TOKEN, type: "auth" }, false); return; case HassSocketMessageTypes.auth_ok: // * Flag as valid connection logger.trace({ name: onMessage }, `event subscriptions starting`); - await sendMessage({ type: HASSIO_WS_COMMAND.subscribe_events }, false); + await sendMessage({ type: "subscribe_events" }, false); onSocketReady(); event.emit(SOCKET_CONNECTED); return; @@ -541,6 +538,22 @@ export function WebsocketAPI({ }; } + function subscribe({ + event_type, + context, + exec, + }: SocketSubscribeOptions) { + hass.socket.sendMessage({ + event_type, + type: "subscribe_events", + }); + hass.socket.onEvent({ + context, + event: event_type, + exec, + }); + } + return { /** * the current state of the websocket @@ -596,6 +609,8 @@ export function WebsocketAPI({ * Applications probably want a higher level function than this */ sendMessage, + + subscribe, /** * remove the current socket connection to home assistant * diff --git a/src/extensions/zone.extension.ts b/src/extensions/zone.extension.ts index 9bef4a8..84765d7 100644 --- a/src/extensions/zone.extension.ts +++ b/src/extensions/zone.extension.ts @@ -1,8 +1,32 @@ -import { TServiceParams } from "@digital-alchemy/core"; +import { is, TServiceParams } from "@digital-alchemy/core"; -import { ManifestItem, ZoneDetails, ZoneOptions } from "../helpers"; +import { ManifestItem, TZoneId, ZoneDetails, ZoneOptions } from "../helpers"; + +export function Zone({ + lifecycle, + config, + hass, + logger, + context, +}: TServiceParams) { + lifecycle.onBootstrap(async () => { + if (!config.hass.AUTO_CONNECT_SOCKET || !config.hass.MANAGE_REGISTRY) { + return; + } + hass.zone.current = await hass.zone.list(); + hass.socket.subscribe({ + context, + event_type: "zone_registry_updated", + async exec() { + hass.zone.current = await hass.zone.list(); + logger.debug(`zone registry updated`); + }, + }); + }); + + is.zone = (floor: string): floor is TZoneId => + hass.zone.current.some(i => i.id === floor); -export function Zone({ hass }: TServiceParams) { async function ZoneCreate(options: ZoneOptions) { await hass.socket.sendMessage({ ...options, @@ -19,13 +43,20 @@ export function Zone({ hass }: TServiceParams) { } async function ZoneList() { - await hass.socket.sendMessage({ + return await hass.socket.sendMessage({ type: "manifest/list", }); } return { create: ZoneCreate, + current: [] as ZoneDetails[], list: ZoneList, update: ZoneUpdate, }; } + +declare module "@digital-alchemy/core" { + export interface IsIt { + zone(floor: string): floor is TZoneId; + } +} diff --git a/src/hass.module.ts b/src/hass.module.ts index 54769a8..b180548 100644 --- a/src/hass.module.ts +++ b/src/hass.module.ts @@ -1,18 +1,19 @@ import { CreateLibrary, StringConfig } from "@digital-alchemy/core"; import { + Area, + Backup, CallProxy, Configure, + Device, EntityManager, FetchAPI, + Floor, + Label, Registry, - Utilities, WebsocketAPI, + Zone, } from "./extensions"; -import { Area } from "./extensions/area.extension"; -import { Device } from "./extensions/device.extension"; -import { Floor } from "./extensions/floor.extension"; -import { Label } from "./extensions/label.extension"; type AllowRestOptions = "prefer" | "allow" | "forbid"; @@ -94,9 +95,12 @@ export const LIB_HASS = CreateLibrary({ }, name: "hass", // no internal dependency ones first - priorityInit: ["fetch", "utils"], + priorityInit: ["fetch"], services: { area: Area, + + backup: Backup, + /** * general service calling interface */ @@ -118,6 +122,7 @@ export const LIB_HASS = CreateLibrary({ */ fetch: FetchAPI, floor: Floor, + label: Label, /** @@ -129,11 +134,7 @@ export const LIB_HASS = CreateLibrary({ * websocket interface */ socket: WebsocketAPI, - - /** - * extra helper functions for interacting with home assistant - */ - utils: Utilities, + zone: Zone, }, }); diff --git a/src/helpers/device.helper.ts b/src/helpers/device.helper.ts new file mode 100644 index 0000000..01b907f --- /dev/null +++ b/src/helpers/device.helper.ts @@ -0,0 +1,24 @@ +export interface DeviceDetails { + area_id: null | string; + configuration_url: null | string; + config_entries: string[]; + connections: Array; + disabled_by: null; + entry_type: EntryType | null; + hw_version: null | string; + id: string; + identifiers: Array>; + labels: string[]; + manufacturer: null | string; + model: null | string; + name_by_user: null | string; + name: string; + serial_number: null; + sw_version: null | string; + via_device_id: TDeviceId; +} + +export enum EntryType { + Service = "service", +} +export type TDeviceId = string & { device: true }; diff --git a/src/helpers/entity-state.helper.ts b/src/helpers/entity-state.helper.ts index 9c6f8f4..4117584 100644 --- a/src/helpers/entity-state.helper.ts +++ b/src/helpers/entity-state.helper.ts @@ -1,3 +1,6 @@ +import { TDeviceId } from "./device.helper"; +import { TAreaId } from "./fetch"; +import { TLabelId } from "./registry"; import { ENTITY_STATE, PICK_ENTITY } from "./utility.helper"; export enum HassEvents { @@ -56,3 +59,47 @@ export type EntityUpdateEvent< time_fired: Date; variables: Record; }; + +export interface EntityDetails { + area_id: TAreaId; + categories: Categories; + config_entry_id: null | string; + device_id: TDeviceId; + disabled_by: string | null; + entity_category: string | null; + entity_id: PICK_ENTITY; + has_entity_name: boolean; + hidden_by: string | null; + icon: null; + id: string; + labels: TLabelId[]; + name: null | string; + options: Options; + original_name: null | string; + platform: TPlatform; + translation_key: null | string; + unique_id: string; +} + +export interface Categories {} + +export interface Options { + conversation: Conversation; + "sensor.private"?: SensorPrivate; + sensor?: Sensor; +} + +export interface Conversation { + should_expose: boolean; +} + +export interface Sensor { + suggested_display_precision?: number; + display_precision?: null; +} + +export interface SensorPrivate { + suggested_unit_of_measurement: string; +} + +export type TPlatform = string; diff --git a/src/helpers/fetch/configuration.ts b/src/helpers/fetch/configuration.ts index e923cd9..544d243 100644 --- a/src/helpers/fetch/configuration.ts +++ b/src/helpers/fetch/configuration.ts @@ -53,3 +53,25 @@ export type AreaCreate = { }; export type TAreaId = string & { area: true }; + +export interface ConfigEntry { + entry_id: string; + domain: string; + title: string; + source: string; + state: State; + supports_options: boolean; + supports_remove_device: boolean; + supports_unload: boolean; + supports_reconfigure: boolean; + pref_disable_new_entities: boolean; + pref_disable_polling: boolean; + disabled_by: null; + reason: null | string; +} + +export enum State { + Loaded = "loaded", + NotLoaded = "not_loaded", + SetupRetry = "setup_retry", +} diff --git a/src/helpers/index.ts b/src/helpers/index.ts index 93022c2..0b6375e 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -1,5 +1,6 @@ export * from "./backup.helper"; export * from "./constants.helper"; +export * from "./device.helper"; export * from "./entity-state.helper"; export * from "./fetch"; export * from "./metrics.helper"; diff --git a/src/helpers/manifest.helper.ts b/src/helpers/manifest.helper.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/helpers/registry.ts b/src/helpers/registry.ts index 5090f71..5825875 100644 --- a/src/helpers/registry.ts +++ b/src/helpers/registry.ts @@ -82,7 +82,9 @@ export type ZoneOptions = { icon: string; passive: boolean; }; -export type ZoneDetails = ZoneOptions & { id: string }; +export type TZoneId = string & { zone: true }; + +export type ZoneDetails = ZoneOptions & { id: TZoneId }; export interface ManifestItem { domain: string; diff --git a/src/helpers/websocket.helper.ts b/src/helpers/websocket.helper.ts index d00325a..6f3ac73 100644 --- a/src/helpers/websocket.helper.ts +++ b/src/helpers/websocket.helper.ts @@ -5,46 +5,6 @@ import { HASSIO_WS_COMMAND, HassSocketMessageTypes } from "./constants.helper"; import { EntityUpdateEvent } from "./entity-state.helper"; import { ALL_DOMAINS, ENTITY_STATE, PICK_ENTITY } from "./utility.helper"; -export interface AreaDTO { - area_id: string; - name: string; -} - -export interface EntityRegistryItem { - area_id: string; - config_entry_id: string; - device_id: string; - disabled_by: string; - entity_id: string; - icon: string; - name: string; - platform: string; -} - -export interface DeviceListItemDTO { - area_id: string; - config_entries: string[]; - connections: string[][]; - disabled_by: null; - entry_type: null; - id: string; - identifiers: string[]; - manufacturer: string; - model: string; - name: string; - name_by_user: null; - sw_version: string; - via_device_id: null; -} - -export interface HassNotificationDTO { - created_at: string; - message: string; - notification_id: string; - status: "unread"; - title: string; -} - export interface SignRequestResponse { path: string; } @@ -58,6 +18,16 @@ export interface SocketMessageDTO { type: HassSocketMessageTypes; } +export type SocketSubscribeOptions = { + event_type: EVENT; + context: TContext; + exec: (data: SocketSubscribeData) => TBlackHole; +}; + +export type SocketSubscribeData = { + event_type: EVENT; +}; + export interface SendSocketMessageDTO { access_token?: string; disabled_by?: "user"; @@ -120,7 +90,7 @@ export interface SignPathDTO { export interface RemoveBackupDTO { slug: string; - type: HASSIO_WS_COMMAND.remove_backup; + type: "backup/remove"; } export interface EntityHistoryDTO< @@ -131,7 +101,7 @@ export interface EntityHistoryDTO< minimal_response?: boolean; no_attributes?: boolean; start_time: Date | string | Dayjs; - type: HASSIO_WS_COMMAND.history_during_period; + type: "history/history_during_period"; } export type EntityHistoryResult< @@ -144,20 +114,6 @@ export type EntityHistoryResult< date: Date; }; -export type SOCKET_MESSAGES = { id?: number } & ( - | FindRelatedDTO - | RegistryGetDTO - | RemoveBackupDTO - | RenderTemplateDTO - | RemoveEntityMessageDTO - | SendSocketMessageDTO - | SignPathDTO - | SubscribeTriggerDTO - | UnsubscribeEventsDTO - | UpdateEntityMessageDTO - | EntityHistoryDTO -); - export type OnHassEventCallback = (event: T) => TBlackHole; export type OnHassEventOptions = { From 58ecb08873b9ac789be68facbfc1af4ce0b3494f Mon Sep 17 00:00:00 2001 From: Zoe Codez Date: Mon, 15 Apr 2024 19:49:21 -0500 Subject: [PATCH 4/5] dynamic ids --- src/dynamic.ts | 6 ++++++ src/extensions/area.extension.ts | 3 ++- src/extensions/device.extension.ts | 10 ++-------- src/extensions/entity.extension.ts | 2 +- src/extensions/floor.extension.ts | 13 ++++--------- src/extensions/label.extension.ts | 13 ++++--------- src/extensions/zone.extension.ts | 21 ++++++++------------- src/hass.module.ts | 2 +- src/helpers/device.helper.ts | 3 ++- src/helpers/fetch/configuration.ts | 4 +--- src/helpers/registry.ts | 6 +----- 11 files changed, 32 insertions(+), 51 deletions(-) diff --git a/src/dynamic.ts b/src/dynamic.ts index 27fc480..159a58c 100644 --- a/src/dynamic.ts +++ b/src/dynamic.ts @@ -5,6 +5,12 @@ // @ts-nocheck import { PICK_ENTITY } from "./helpers"; +export type TFloorId = string & { floor: boolean }; +export type TAreaId = string & { area: true }; +export type TLabelId = string & { label: string }; +export type TDeviceId = string & { device: true }; +export type TZoneId = string & { zone: true }; + /** * ## THIS FILE IS INTENDED TO BE REPLACED * diff --git a/src/extensions/area.extension.ts b/src/extensions/area.extension.ts index 0d50138..1a7acf3 100644 --- a/src/extensions/area.extension.ts +++ b/src/extensions/area.extension.ts @@ -1,6 +1,7 @@ import { TServiceParams } from "@digital-alchemy/core"; -import { AreaCreate, AreaDetails, TAreaId } from "../helpers"; +import { TAreaId } from "../dynamic"; +import { AreaCreate, AreaDetails } from "../helpers"; export function Area({ hass, context, config, logger }: TServiceParams) { hass.socket.onConnect(async () => { diff --git a/src/extensions/device.extension.ts b/src/extensions/device.extension.ts index 505d3a7..06babb6 100644 --- a/src/extensions/device.extension.ts +++ b/src/extensions/device.extension.ts @@ -2,14 +2,8 @@ import { TServiceParams } from "@digital-alchemy/core"; import { DeviceDetails } from "../helpers"; -export function Device({ - hass, - config, - lifecycle, - context, - logger, -}: TServiceParams) { - lifecycle.onBootstrap(async () => { +export function Device({ hass, config, context, logger }: TServiceParams) { + hass.socket.onConnect(async () => { if (!config.hass.AUTO_CONNECT_SOCKET || !config.hass.MANAGE_REGISTRY) { return; } diff --git a/src/extensions/entity.extension.ts b/src/extensions/entity.extension.ts index 2f0c5f8..9ed379a 100644 --- a/src/extensions/entity.extension.ts +++ b/src/extensions/entity.extension.ts @@ -413,7 +413,7 @@ export function EntityManager({ }); } - lifecycle.onBootstrap(async () => { + hass.socket.onConnect(async () => { if (!config.hass.AUTO_CONNECT_SOCKET || !config.hass.MANAGE_REGISTRY) { return; } diff --git a/src/extensions/floor.extension.ts b/src/extensions/floor.extension.ts index b8e18e2..d8ba59f 100644 --- a/src/extensions/floor.extension.ts +++ b/src/extensions/floor.extension.ts @@ -1,15 +1,10 @@ import { is, TServiceParams } from "@digital-alchemy/core"; -import { FloorCreate, FloorDetails, TFloorId } from "../helpers"; +import { TFloorId } from "../dynamic"; +import { FloorCreate, FloorDetails } from "../helpers"; -export function Floor({ - hass, - lifecycle, - config, - context, - logger, -}: TServiceParams) { - lifecycle.onBootstrap(async () => { +export function Floor({ hass, config, context, logger }: TServiceParams) { + hass.socket.onConnect(async () => { if (!config.hass.AUTO_CONNECT_SOCKET || !config.hass.MANAGE_REGISTRY) { return; } diff --git a/src/extensions/label.extension.ts b/src/extensions/label.extension.ts index fc1d7f5..ea8a7ec 100644 --- a/src/extensions/label.extension.ts +++ b/src/extensions/label.extension.ts @@ -1,15 +1,10 @@ import { is, TServiceParams } from "@digital-alchemy/core"; -import { LabelDefinition, LabelOptions, TLabelId } from "../helpers"; +import { TLabelId } from "../dynamic"; +import { LabelDefinition, LabelOptions } from "../helpers"; -export function Label({ - hass, - lifecycle, - config, - logger, - context, -}: TServiceParams) { - lifecycle.onBootstrap(async () => { +export function Label({ hass, config, logger, context }: TServiceParams) { + hass.socket.onConnect(async () => { if (!config.hass.AUTO_CONNECT_SOCKET || !config.hass.MANAGE_REGISTRY) { return; } diff --git a/src/extensions/zone.extension.ts b/src/extensions/zone.extension.ts index 84765d7..0dab0b3 100644 --- a/src/extensions/zone.extension.ts +++ b/src/extensions/zone.extension.ts @@ -1,15 +1,10 @@ import { is, TServiceParams } from "@digital-alchemy/core"; -import { ManifestItem, TZoneId, ZoneDetails, ZoneOptions } from "../helpers"; +import { TZoneId } from "../dynamic"; +import { ManifestItem, ZoneDetails, ZoneOptions } from "../helpers"; -export function Zone({ - lifecycle, - config, - hass, - logger, - context, -}: TServiceParams) { - lifecycle.onBootstrap(async () => { +export function Zone({ config, hass, logger, context }: TServiceParams) { + hass.socket.onConnect(async () => { if (!config.hass.AUTO_CONNECT_SOCKET || !config.hass.MANAGE_REGISTRY) { return; } @@ -24,8 +19,8 @@ export function Zone({ }); }); - is.zone = (floor: string): floor is TZoneId => - hass.zone.current.some(i => i.id === floor); + is.zone = (zone: string): zone is TZoneId => + hass.zone.current.some(i => i.id === zone); async function ZoneCreate(options: ZoneOptions) { await hass.socket.sendMessage({ @@ -44,7 +39,7 @@ export function Zone({ async function ZoneList() { return await hass.socket.sendMessage({ - type: "manifest/list", + type: "zone/list", }); } return { @@ -57,6 +52,6 @@ export function Zone({ declare module "@digital-alchemy/core" { export interface IsIt { - zone(floor: string): floor is TZoneId; + zone(zone: string): zone is TZoneId; } } diff --git a/src/hass.module.ts b/src/hass.module.ts index b180548..b60be6f 100644 --- a/src/hass.module.ts +++ b/src/hass.module.ts @@ -95,7 +95,7 @@ export const LIB_HASS = CreateLibrary({ }, name: "hass", // no internal dependency ones first - priorityInit: ["fetch"], + priorityInit: ["fetch", "socket"], services: { area: Area, diff --git a/src/helpers/device.helper.ts b/src/helpers/device.helper.ts index 01b907f..96ca00e 100644 --- a/src/helpers/device.helper.ts +++ b/src/helpers/device.helper.ts @@ -1,3 +1,5 @@ +import { TDeviceId } from "../dynamic"; + export interface DeviceDetails { area_id: null | string; configuration_url: null | string; @@ -21,4 +23,3 @@ export interface DeviceDetails { export enum EntryType { Service = "service", } -export type TDeviceId = string & { device: true }; diff --git a/src/helpers/fetch/configuration.ts b/src/helpers/fetch/configuration.ts index 544d243..8329642 100644 --- a/src/helpers/fetch/configuration.ts +++ b/src/helpers/fetch/configuration.ts @@ -1,4 +1,4 @@ -import { TFloorId, TLabelId } from "../registry"; +import { TAreaId, TFloorId, TLabelId } from "../../dynamic"; export interface HassUnitSystem { length: "mi"; @@ -52,8 +52,6 @@ export type AreaCreate = { picture: string; }; -export type TAreaId = string & { area: true }; - export interface ConfigEntry { entry_id: string; domain: string; diff --git a/src/helpers/registry.ts b/src/helpers/registry.ts index 5825875..1f21cd0 100644 --- a/src/helpers/registry.ts +++ b/src/helpers/registry.ts @@ -1,3 +1,4 @@ +import { TFloorId, TLabelId, TZoneId } from "../dynamic"; import { PICK_ENTITY } from "./utility.helper"; export type LabelOptions = { @@ -16,8 +17,6 @@ export type EditAliasOptions = { alias: string; }; -export type TLabelId = string & { label: string }; - export type LabelDefinition = { color: string; description?: string; @@ -82,7 +81,6 @@ export type ZoneOptions = { icon: string; passive: boolean; }; -export type TZoneId = string & { zone: true }; export type ZoneDetails = ZoneOptions & { id: TZoneId }; @@ -169,8 +167,6 @@ export type FloorCreate = { aliases: string[]; }; -export type TFloorId = string & { floor: boolean }; - export type FloorDetails = FloorCreate & { floor_id: TFloorId; }; From 09a854be70cfe6262f4a34ee520d649b7f6392d9 Mon Sep 17 00:00:00 2001 From: Zoe Codez Date: Mon, 15 Apr 2024 19:56:27 -0500 Subject: [PATCH 5/5] lint, version bump --- cspell.config.yaml | 5 ++++- package.json | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/cspell.config.yaml b/cspell.config.yaml index 9e6b02e..99fa926 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -12,6 +12,9 @@ words: - macaddress - quickboot - ssdp - - SYSTYPE + - systype + - zeroconf + - homeassistant + - rrule ignoreWords: [] import: [] diff --git a/package.json b/package.json index ccc362b..2acc3c3 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@digital-alchemy/hass", "repository": "https://github.com/Digital-Alchemy-TS/hass", "homepage": "https://docs.digital-alchemy.app/Hass", - "version": "0.3.9", + "version": "0.3.10", "scripts": { "build": "rm -rf dist/; tsc", "lint": "eslint src",