@digital-alchemy/hass 24.9.4 → 24.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/helpers/notify.helper.d.ts +2 -2
- package/package.json +16 -14
- package/scripts/mock-assistant.sh +5 -0
- package/scripts/run-e2e.sh +7 -0
- package/scripts/test.sh +2 -0
- package/src/dynamic.ts +4254 -0
- package/src/extensions/area.extension.ts +118 -0
- package/src/extensions/backup.extension.ts +63 -0
- package/src/extensions/call-proxy.extension.ts +113 -0
- package/src/extensions/config.extension.ts +119 -0
- package/src/extensions/conversation.extension.ts +46 -0
- package/src/extensions/device.extension.ts +56 -0
- package/src/extensions/entity.extension.ts +344 -0
- package/src/extensions/events.extension.ts +25 -0
- package/src/extensions/fetch-api.extension.ts +269 -0
- package/src/extensions/floor.extension.ts +76 -0
- package/src/extensions/id-by.extension.ts +157 -0
- package/src/extensions/index.ts +16 -0
- package/src/extensions/internal.extension.ts +145 -0
- package/src/extensions/label.extension.ts +83 -0
- package/src/extensions/reference.extension.ts +330 -0
- package/src/extensions/registry.extension.ts +44 -0
- package/src/extensions/websocket-api.extension.ts +554 -0
- package/src/extensions/zone.extension.ts +69 -0
- package/src/hass.module.ts +217 -0
- package/src/helpers/backup.helper.ts +11 -0
- package/src/helpers/constants.helper.ts +30 -0
- package/src/helpers/device.helper.ts +25 -0
- package/src/helpers/entity-state.helper.ts +171 -0
- package/src/helpers/features.helper.ts +580 -0
- package/src/helpers/fetch/calendar.ts +54 -0
- package/src/helpers/fetch/configuration.ts +75 -0
- package/src/helpers/fetch/index.ts +5 -0
- package/src/helpers/fetch/server-log.ts +28 -0
- package/src/helpers/fetch/service-list.ts +64 -0
- package/src/helpers/fetch/weather-forecasts.ts +86 -0
- package/src/helpers/fetch.helper.ts +328 -0
- package/src/helpers/id-by.helper.ts +53 -0
- package/src/helpers/index.ts +13 -0
- package/src/helpers/interfaces.helper.ts +340 -0
- package/src/helpers/manifest.helper.ts +0 -0
- package/src/helpers/notify.helper.ts +302 -0
- package/src/helpers/registry.ts +281 -0
- package/src/helpers/utility.helper.ts +147 -0
- package/src/helpers/websocket.helper.ts +117 -0
- package/src/index.ts +5 -0
- package/src/mock_assistant/extensions/area.extension.ts +62 -0
- package/src/mock_assistant/extensions/config.extension.ts +33 -0
- package/src/mock_assistant/extensions/device.extension.ts +44 -0
- package/src/mock_assistant/extensions/entity-registry.extension.ts +41 -0
- package/src/mock_assistant/extensions/entity.extension.ts +114 -0
- package/src/mock_assistant/extensions/events.extension.ts +37 -0
- package/src/mock_assistant/extensions/fetch.extension.ts +3 -0
- package/src/mock_assistant/extensions/fixtures.extension.ts +79 -0
- package/src/mock_assistant/extensions/floor.extension.ts +64 -0
- package/src/mock_assistant/extensions/index.ts +12 -0
- package/src/mock_assistant/extensions/label.extension.ts +64 -0
- package/src/mock_assistant/extensions/services.extension.ts +25 -0
- package/src/mock_assistant/extensions/websocket-api.extension.ts +84 -0
- package/src/mock_assistant/extensions/zone.extension.ts +65 -0
- package/src/mock_assistant/helpers/fixtures.ts +22 -0
- package/src/mock_assistant/helpers/index.ts +1 -0
- package/src/mock_assistant/index.ts +3 -0
- package/src/mock_assistant/main.ts +46 -0
- package/src/mock_assistant/mock-assistant.module.ts +90 -0
- package/src/quickboot.module.ts +23 -0
- package/src/testing/area.spec.ts +189 -0
- package/src/testing/backup.spec.ts +157 -0
- package/src/testing/config.spec.ts +188 -0
- package/src/testing/device.spec.ts +89 -0
- package/src/testing/entity.spec.ts +171 -0
- package/src/testing/events.spec.ts +78 -0
- package/src/testing/fetch-api.spec.ts +410 -0
- package/src/testing/fixtures.spec.ts +158 -0
- package/src/testing/floor.spec.ts +186 -0
- package/src/testing/id-by.spec.ts +140 -0
- package/src/testing/label.spec.ts +186 -0
- package/src/testing/ref-by.spec.ts +300 -0
- package/src/testing/websocket.spec.ts +63 -0
- package/src/testing/workflow.spec.ts +195 -0
- package/src/testing/zone.spec.ts +109 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { debounce, eachSeries, InternalError, TServiceParams } from "@digital-alchemy/core";
|
|
2
|
+
|
|
3
|
+
import { TAreaId } from "../dynamic";
|
|
4
|
+
import {
|
|
5
|
+
ANY_ENTITY,
|
|
6
|
+
AREA_REGISTRY_UPDATED,
|
|
7
|
+
AreaCreate,
|
|
8
|
+
AreaDetails,
|
|
9
|
+
EARLY_ON_READY,
|
|
10
|
+
ENTITY_REGISTRY_UPDATED,
|
|
11
|
+
HassAreaService,
|
|
12
|
+
} from "../helpers";
|
|
13
|
+
|
|
14
|
+
export function Area({
|
|
15
|
+
hass,
|
|
16
|
+
context,
|
|
17
|
+
config,
|
|
18
|
+
logger,
|
|
19
|
+
event,
|
|
20
|
+
lifecycle,
|
|
21
|
+
}: TServiceParams): HassAreaService {
|
|
22
|
+
hass.socket.onConnect(async () => {
|
|
23
|
+
let loading = new Promise<void>(async done => {
|
|
24
|
+
hass.area.current = await hass.area.list();
|
|
25
|
+
loading = undefined;
|
|
26
|
+
done();
|
|
27
|
+
});
|
|
28
|
+
lifecycle.onReady(async () => loading && (await loading), EARLY_ON_READY);
|
|
29
|
+
|
|
30
|
+
hass.socket.subscribe({
|
|
31
|
+
context,
|
|
32
|
+
event_type: "area_registry_updated",
|
|
33
|
+
async exec() {
|
|
34
|
+
await debounce(AREA_REGISTRY_UPDATED, config.hass.EVENT_DEBOUNCE_MS);
|
|
35
|
+
hass.area.current = await hass.area.list();
|
|
36
|
+
logger.debug(`area registry updated`);
|
|
37
|
+
event.emit(AREA_REGISTRY_UPDATED);
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
async function list() {
|
|
43
|
+
return await hass.socket.sendMessage<AreaDetails[]>({
|
|
44
|
+
type: "config/area_registry/list",
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function deleteArea(area_id: TAreaId) {
|
|
49
|
+
return await new Promise<void>(async done => {
|
|
50
|
+
event.once(AREA_REGISTRY_UPDATED, done);
|
|
51
|
+
await hass.socket.sendMessage({
|
|
52
|
+
area_id,
|
|
53
|
+
type: "config/area_registry/delete",
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function create(details: AreaCreate) {
|
|
59
|
+
return await new Promise<void>(async done => {
|
|
60
|
+
event.once(AREA_REGISTRY_UPDATED, done);
|
|
61
|
+
await hass.socket.sendMessage({
|
|
62
|
+
floor_id: "",
|
|
63
|
+
icon: "",
|
|
64
|
+
labels: [],
|
|
65
|
+
picture: "",
|
|
66
|
+
type: "config/area_registry/create",
|
|
67
|
+
...details,
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function apply(area: TAreaId, entities: ANY_ENTITY[]) {
|
|
73
|
+
const out = { updated: [] as ANY_ENTITY[] };
|
|
74
|
+
await eachSeries(entities, async (entity: ANY_ENTITY) => {
|
|
75
|
+
const details = hass.entity.registry.current.find(item => item.entity_id === entity);
|
|
76
|
+
if (!details) {
|
|
77
|
+
throw new InternalError(
|
|
78
|
+
context,
|
|
79
|
+
"UNKNOWN_ENTITY",
|
|
80
|
+
`Cannot find ${entity} in entity registry`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
if (details.area_id === area) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
await new Promise<void>(async done => {
|
|
87
|
+
event.once(ENTITY_REGISTRY_UPDATED, done);
|
|
88
|
+
logger.trace({ area, entity }, `setting area`);
|
|
89
|
+
out.updated.push(entity);
|
|
90
|
+
await hass.socket.sendMessage({
|
|
91
|
+
area_id: area,
|
|
92
|
+
entity_id: entity,
|
|
93
|
+
type: "config/entity_registry/update",
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function update(details: AreaDetails) {
|
|
101
|
+
return await new Promise<void>(async done => {
|
|
102
|
+
event.once(AREA_REGISTRY_UPDATED, done);
|
|
103
|
+
await hass.socket.sendMessage({
|
|
104
|
+
type: "config/area_registry/update",
|
|
105
|
+
...details,
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
apply,
|
|
112
|
+
create,
|
|
113
|
+
current: [] as AreaDetails[],
|
|
114
|
+
delete: deleteArea,
|
|
115
|
+
list,
|
|
116
|
+
update,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { is, SECOND, sleep, TServiceParams } from "@digital-alchemy/core";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
BackupResponse,
|
|
5
|
+
HassBackupService,
|
|
6
|
+
HomeAssistantBackup,
|
|
7
|
+
SignRequestResponse,
|
|
8
|
+
} from "../helpers";
|
|
9
|
+
|
|
10
|
+
export function Backup({ logger, hass, config }: TServiceParams): HassBackupService {
|
|
11
|
+
async function download(slug: string, destination: string): Promise<void> {
|
|
12
|
+
const result = await hass.socket.sendMessage<SignRequestResponse>({
|
|
13
|
+
path: `/api/backup/download/${slug}`,
|
|
14
|
+
type: "auth/sign_path",
|
|
15
|
+
});
|
|
16
|
+
if (!is.object(result) || !("path" in result)) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
await hass.fetch.download(destination, {
|
|
20
|
+
url: result.path,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function generate(): Promise<HomeAssistantBackup> {
|
|
25
|
+
let current = await hass.backup.list();
|
|
26
|
+
// const originalLength = current.backups.length;
|
|
27
|
+
if (current.backing_up) {
|
|
28
|
+
logger.warn(
|
|
29
|
+
{ name: generate },
|
|
30
|
+
`a backup is currently in progress. waiting for that to complete instead`,
|
|
31
|
+
);
|
|
32
|
+
} else {
|
|
33
|
+
logger.info({ name: generate }, "initiating new backup");
|
|
34
|
+
hass.socket.sendMessage({ type: "backup/generate" });
|
|
35
|
+
while (current.backing_up === false) {
|
|
36
|
+
logger.debug({ name: generate }, "... waiting");
|
|
37
|
+
await sleep(config.hass.RETRY_INTERVAL * SECOND);
|
|
38
|
+
current = await hass.backup.list();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
while (current.backing_up === true) {
|
|
42
|
+
logger.debug({ name: generate }, "... waiting");
|
|
43
|
+
await sleep(config.hass.RETRY_INTERVAL * SECOND);
|
|
44
|
+
current = await hass.backup.list();
|
|
45
|
+
}
|
|
46
|
+
logger.info({ name: generate }, `backup complete`);
|
|
47
|
+
return current.backups.pop();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function list(): Promise<BackupResponse> {
|
|
51
|
+
logger.trace({ name: list }, "send");
|
|
52
|
+
return await hass.socket.sendMessage<BackupResponse>({
|
|
53
|
+
type: "backup/info",
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function remove(slug: string): Promise<void> {
|
|
58
|
+
logger.trace({ name: remove }, "send");
|
|
59
|
+
await hass.socket.sendMessage({ slug, type: "backup/remove" });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { download, generate, list, remove };
|
|
63
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { is, TServiceParams } from "@digital-alchemy/core";
|
|
2
|
+
|
|
3
|
+
import { ALL_SERVICE_DOMAINS, iCallService, PICK_SERVICE, PICK_SERVICE_PARAMETERS } from "..";
|
|
4
|
+
|
|
5
|
+
export function CallProxy({ logger, lifecycle, internal, hass }: TServiceParams): iCallService {
|
|
6
|
+
let loaded = false;
|
|
7
|
+
const rawProxy = {} as Record<string, Record<string, unknown>>;
|
|
8
|
+
/**
|
|
9
|
+
* Describe the current services, and build up a proxy api based on that.
|
|
10
|
+
*
|
|
11
|
+
* This API matches the api at the time the this function is run, which may be different from any generated typescript definitions from the past.
|
|
12
|
+
*/
|
|
13
|
+
lifecycle.onBootstrap(async () => {
|
|
14
|
+
logger.debug({ name: "onBootstrap" }, `runtime populate service interfaces`);
|
|
15
|
+
await loadServiceList();
|
|
16
|
+
loaded = true;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
async function loadServiceList(): Promise<void> {
|
|
20
|
+
await hass.configure.loadServiceList();
|
|
21
|
+
const services = hass.configure.getServices();
|
|
22
|
+
services.forEach(value => {
|
|
23
|
+
const services = Object.keys(value.services);
|
|
24
|
+
|
|
25
|
+
rawProxy[value.domain] = Object.fromEntries(
|
|
26
|
+
Object.entries(value.services).map(([key]) => [
|
|
27
|
+
key,
|
|
28
|
+
async <SERVICE extends PICK_SERVICE<ALL_SERVICE_DOMAINS>>(parameters: object) => {
|
|
29
|
+
const data = value.services[key];
|
|
30
|
+
|
|
31
|
+
const service = `${value.domain}.${key}` as SERVICE;
|
|
32
|
+
return await sendMessage(
|
|
33
|
+
service,
|
|
34
|
+
{
|
|
35
|
+
...parameters,
|
|
36
|
+
} as PICK_SERVICE_PARAMETERS<ALL_SERVICE_DOMAINS, SERVICE>,
|
|
37
|
+
is.boolean(data?.response?.optional),
|
|
38
|
+
);
|
|
39
|
+
},
|
|
40
|
+
]),
|
|
41
|
+
);
|
|
42
|
+
logger.trace({ name: loadServiceList, services }, `loaded domain [%s]`, value.domain);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Prefer sending via socket, if available.
|
|
48
|
+
*/
|
|
49
|
+
async function sendMessage<
|
|
50
|
+
DOMAIN extends ALL_SERVICE_DOMAINS,
|
|
51
|
+
SERVICE extends PICK_SERVICE<DOMAIN>,
|
|
52
|
+
>(
|
|
53
|
+
serviceName: SERVICE,
|
|
54
|
+
service_data: PICK_SERVICE_PARAMETERS<DOMAIN, SERVICE>,
|
|
55
|
+
return_response: boolean,
|
|
56
|
+
) {
|
|
57
|
+
// pause for rest also
|
|
58
|
+
if (hass.socket.pauseMessages) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
const [domain, service] = serviceName.split(".");
|
|
62
|
+
// User can just not await this call if they don't care about the "waitForChange"
|
|
63
|
+
|
|
64
|
+
if (!return_response) {
|
|
65
|
+
return await hass.socket.sendMessage(
|
|
66
|
+
{ domain, service, service_data, type: "call_service" },
|
|
67
|
+
true,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
const result = (await hass.socket.sendMessage(
|
|
71
|
+
{ domain, return_response, service, service_data, type: "call_service" },
|
|
72
|
+
true,
|
|
73
|
+
)) as { response: unknown };
|
|
74
|
+
if (!result?.response) {
|
|
75
|
+
logger.warn({ result }, `{%s}.{%s} did not return a response`, domain, service);
|
|
76
|
+
}
|
|
77
|
+
return result?.response;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function buildCallProxy(): iCallService {
|
|
81
|
+
return new Proxy(rawProxy as iCallService, {
|
|
82
|
+
get: (_, domain: ALL_SERVICE_DOMAINS) => {
|
|
83
|
+
// oddities in the way proxies work
|
|
84
|
+
// this situation isn't testable afaik
|
|
85
|
+
if (!internal.boot.constructComplete.has("hass")) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
if (!loaded) {
|
|
89
|
+
lifecycle.onReady(() => {
|
|
90
|
+
logger.error(
|
|
91
|
+
`attempted to use {hass.call} before data loaded. use {lifecycle.onReady}`,
|
|
92
|
+
);
|
|
93
|
+
// eslint-disable-next-line no-console
|
|
94
|
+
console.trace(`hass.call`);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return rawProxy[domain];
|
|
98
|
+
},
|
|
99
|
+
has(_, property: string) {
|
|
100
|
+
return property in rawProxy;
|
|
101
|
+
},
|
|
102
|
+
ownKeys() {
|
|
103
|
+
return Object.keys(rawProxy);
|
|
104
|
+
},
|
|
105
|
+
set() {
|
|
106
|
+
// lol, no
|
|
107
|
+
return false;
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return buildCallProxy();
|
|
113
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-magic-numbers */
|
|
2
|
+
import {
|
|
3
|
+
asyncNoop,
|
|
4
|
+
INCREMENT,
|
|
5
|
+
is,
|
|
6
|
+
SECOND,
|
|
7
|
+
sleep,
|
|
8
|
+
START,
|
|
9
|
+
TServiceParams,
|
|
10
|
+
} from "@digital-alchemy/core";
|
|
11
|
+
import { env } from "process";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
ALL_SERVICE_DOMAINS,
|
|
15
|
+
HassConfigService,
|
|
16
|
+
HassServiceDTO,
|
|
17
|
+
iCallService,
|
|
18
|
+
PostConfigPriorities,
|
|
19
|
+
} from "..";
|
|
20
|
+
|
|
21
|
+
const MAX_ATTEMPTS = 50;
|
|
22
|
+
const FAILED = 1;
|
|
23
|
+
|
|
24
|
+
export const SERVICE_LIST_UPDATED = "SERVICE_LIST_UPDATED";
|
|
25
|
+
|
|
26
|
+
export function Configure({
|
|
27
|
+
logger,
|
|
28
|
+
lifecycle,
|
|
29
|
+
event,
|
|
30
|
+
hass,
|
|
31
|
+
config,
|
|
32
|
+
internal,
|
|
33
|
+
}: TServiceParams): HassConfigService {
|
|
34
|
+
lifecycle.onPreInit(() => {
|
|
35
|
+
// HASSIO_TOKEN provided by home assistant to addons
|
|
36
|
+
// SUPERVISOR_TOKEN used as alias elsewhere
|
|
37
|
+
const token = env.HASSIO_TOKEN || env.SUPERVISOR_TOKEN;
|
|
38
|
+
if (is.empty(token)) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
logger.debug({ name: "onPreInit" }, `auto configuring from addon environment`);
|
|
42
|
+
internal.boilerplate.configuration.set(
|
|
43
|
+
"hass",
|
|
44
|
+
"BASE_URL",
|
|
45
|
+
// don't go over the network
|
|
46
|
+
env.HASS_SERVER || "http://supervisor/core",
|
|
47
|
+
);
|
|
48
|
+
internal.boilerplate.configuration.set("hass", "TOKEN", token);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Request by someone to validate the provided credentials are valid
|
|
53
|
+
*
|
|
54
|
+
* Send a test request, and provide feedback on what happened
|
|
55
|
+
*/
|
|
56
|
+
lifecycle.onPostConfig(async () => {
|
|
57
|
+
if (!config.hass.VALIDATE_CONFIGURATION) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
internal.boilerplate.configuration.set("boilerplate", "LOG_LEVEL", "trace");
|
|
61
|
+
await asyncNoop();
|
|
62
|
+
logger.info({ name: "onPostConfig" }, `validating credentials`);
|
|
63
|
+
try {
|
|
64
|
+
const result = await hass.fetch.checkCredentials();
|
|
65
|
+
if (is.object(result)) {
|
|
66
|
+
// * all good
|
|
67
|
+
logger.info({ name: "onPostConfig" }, result.message);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
// * bad token
|
|
71
|
+
logger.error({ name: "onPostConfig" }, String(result));
|
|
72
|
+
process.exit(0);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
// * bad BASE_URL
|
|
75
|
+
logger.error({ error, name: "onPostConfig" }, "failed to send request");
|
|
76
|
+
process.exit(0);
|
|
77
|
+
}
|
|
78
|
+
}, PostConfigPriorities.VALIDATE);
|
|
79
|
+
|
|
80
|
+
let services: HassServiceDTO[];
|
|
81
|
+
async function loadServiceList(recursion = START): Promise<void> {
|
|
82
|
+
logger.debug({ name: loadServiceList }, `fetching service list`);
|
|
83
|
+
services = await hass.fetch.listServices();
|
|
84
|
+
if (is.empty(services)) {
|
|
85
|
+
if (recursion > MAX_ATTEMPTS) {
|
|
86
|
+
logger.fatal({ name: loadServiceList }, `failed to load service list from Home Assistant`);
|
|
87
|
+
process.exit(FAILED);
|
|
88
|
+
}
|
|
89
|
+
logger.warn(
|
|
90
|
+
{ name: loadServiceList },
|
|
91
|
+
"failed to retrieve {service} list. Retrying {%s}/[%s]",
|
|
92
|
+
recursion,
|
|
93
|
+
MAX_ATTEMPTS,
|
|
94
|
+
);
|
|
95
|
+
await sleep(config.hass.RETRY_INTERVAL * SECOND);
|
|
96
|
+
await loadServiceList(recursion + INCREMENT);
|
|
97
|
+
} else {
|
|
98
|
+
event.emit(SERVICE_LIST_UPDATED, services);
|
|
99
|
+
checkedServices = new Map();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
let checkedServices = new Map<string, boolean>();
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
getServices: () => services,
|
|
106
|
+
isService: <DOMAIN extends ALL_SERVICE_DOMAINS>(
|
|
107
|
+
domain: DOMAIN,
|
|
108
|
+
service: string,
|
|
109
|
+
): service is Extract<keyof iCallService[DOMAIN], string> => {
|
|
110
|
+
if (checkedServices.has(service)) {
|
|
111
|
+
return checkedServices.get(service);
|
|
112
|
+
}
|
|
113
|
+
const exists = services.some(i => i.domain === domain && !is.undefined(i.services[service]));
|
|
114
|
+
checkedServices.set(service, exists);
|
|
115
|
+
return exists;
|
|
116
|
+
},
|
|
117
|
+
loadServiceList,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { TServiceParams } from "@digital-alchemy/core";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
EditAliasOptions,
|
|
5
|
+
HassConversationService,
|
|
6
|
+
ToggleExpose,
|
|
7
|
+
UPDATE_REGISTRY,
|
|
8
|
+
} from "../helpers";
|
|
9
|
+
|
|
10
|
+
export function Conversation({ hass, logger }: TServiceParams): HassConversationService {
|
|
11
|
+
async function addAlias({ entity, alias }: EditAliasOptions) {
|
|
12
|
+
const current = await hass.entity.registry.get(entity);
|
|
13
|
+
if (current?.aliases?.includes(alias)) {
|
|
14
|
+
logger.debug({ name: entity }, `already has alias {%s}`, alias);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
await hass.socket.sendMessage({
|
|
18
|
+
entity_id: entity,
|
|
19
|
+
type: "config/entity_registry/update",
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function removeAlias({ entity, alias }: EditAliasOptions) {
|
|
24
|
+
const current = await hass.entity.registry.get(entity);
|
|
25
|
+
if (!current?.aliases?.includes(alias)) {
|
|
26
|
+
logger.debug({ name: entity }, `does not have alias {%s}`, alias);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
await hass.socket.sendMessage({ entity_id: entity, type: UPDATE_REGISTRY });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function setConversational({ entity_ids, assistants, should_expose }: ToggleExpose) {
|
|
33
|
+
await hass.socket.sendMessage({
|
|
34
|
+
assistants: [assistants].flat(),
|
|
35
|
+
entity_ids: [entity_ids].flat(),
|
|
36
|
+
should_expose,
|
|
37
|
+
type: UPDATE_REGISTRY,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
addAlias,
|
|
43
|
+
removeAlias,
|
|
44
|
+
setConversational,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { debounce, TServiceParams } from "@digital-alchemy/core";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
DEVICE_REGISTRY_UPDATED,
|
|
5
|
+
DeviceDetails,
|
|
6
|
+
EARLY_ON_READY,
|
|
7
|
+
HassDeviceService,
|
|
8
|
+
} from "../helpers";
|
|
9
|
+
|
|
10
|
+
export function Device({
|
|
11
|
+
hass,
|
|
12
|
+
config,
|
|
13
|
+
context,
|
|
14
|
+
logger,
|
|
15
|
+
lifecycle,
|
|
16
|
+
event,
|
|
17
|
+
}: TServiceParams): HassDeviceService {
|
|
18
|
+
hass.socket.onConnect(async () => {
|
|
19
|
+
let loading = new Promise<void>(async done => {
|
|
20
|
+
hass.device.current = await hass.device.list();
|
|
21
|
+
loading = undefined;
|
|
22
|
+
done();
|
|
23
|
+
});
|
|
24
|
+
lifecycle.onReady(async () => loading && (await loading), EARLY_ON_READY);
|
|
25
|
+
await subscribeUpdates();
|
|
26
|
+
|
|
27
|
+
hass.socket.subscribe({
|
|
28
|
+
context,
|
|
29
|
+
event_type: "device_registry_updated",
|
|
30
|
+
async exec() {
|
|
31
|
+
await debounce(DEVICE_REGISTRY_UPDATED, config.hass.EVENT_DEBOUNCE_MS);
|
|
32
|
+
hass.device.current = await hass.device.list();
|
|
33
|
+
logger.debug(`device registry updated`);
|
|
34
|
+
event.emit(DEVICE_REGISTRY_UPDATED);
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
async function subscribeUpdates() {
|
|
40
|
+
await hass.socket.sendMessage({
|
|
41
|
+
event_type: "device_registry_updated",
|
|
42
|
+
type: "subscribe_events",
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function list() {
|
|
47
|
+
return await hass.socket.sendMessage<DeviceDetails[]>({
|
|
48
|
+
type: "config/device_registry/list",
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
current: [] as DeviceDetails[],
|
|
54
|
+
list,
|
|
55
|
+
};
|
|
56
|
+
}
|