Skip to content

Commit 34fe4d7

Browse files
authored
Add domain property to entity proxies (#172)
## 📬 Changes - added `domain` property to aid in filtering synapse entities ## 🗒️ Checklist <!-- please check all items and add your own --> - [x] Read the contribution [guide](../CONTRIBUTING.md) and accept the [code](../CODE_OF_CONDUCT.md) of conduct - [ ] Readme and [docs](https://docs.digital-alchemy.app/) (updated or not needed) - [x] Tests (added, updated or not needed)
1 parent 19fbc9d commit 34fe4d7

5 files changed

Lines changed: 1525 additions & 1322 deletions

File tree

package.json

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,10 @@
3838
},
3939
"license": "MIT",
4040
"dependencies": {
41-
"better-sqlite3": "^12.4.6",
42-
"drizzle-orm": "^0.44.7",
43-
"mysql2": "^3.15.3",
44-
"postgres": "^3.4.7"
41+
"better-sqlite3": "^12.6.0",
42+
"drizzle-orm": "^0.45.1",
43+
"mysql2": "^3.16.0",
44+
"postgres": "^3.4.8"
4545
},
4646
"peerDependencies": {
4747
"@digital-alchemy/core": "*",
@@ -50,28 +50,28 @@
5050
"uuid": "*"
5151
},
5252
"devDependencies": {
53-
"@cspell/eslint-plugin": "^9.3.2",
54-
"@digital-alchemy/core": "^25.11.23",
53+
"@cspell/eslint-plugin": "^9.4.0",
54+
"@digital-alchemy/core": "^26.1.9",
5555
"@digital-alchemy/hass": "^25.11.27",
56-
"@eslint/compat": "^2.0.0",
57-
"@eslint/eslintrc": "^3.3.1",
58-
"@eslint/js": "^9.39.1",
56+
"@eslint/compat": "^2.0.1",
57+
"@eslint/eslintrc": "^3.3.3",
58+
"@eslint/js": "^9.39.2",
5959
"@types/better-sqlite3": "^7.6.13",
60-
"@types/bun": "^1.3.3",
61-
"@types/node": "^24.10.1",
60+
"@types/bun": "^1.3.5",
61+
"@types/node": "^25.0.6",
6262
"@types/node-cron": "^3.0.11",
63-
"@types/pg": "^8.15.6",
63+
"@types/pg": "^8.16.0",
6464
"@types/uuid": "^11.0.0",
6565
"@types/ws": "^8.18.1",
66-
"@typescript-eslint/eslint-plugin": "8.48.0",
67-
"@typescript-eslint/parser": "8.48.0",
68-
"@vitest/coverage-v8": "^4.0.14",
69-
"@vitest/ui": "^4.0.14",
70-
"bun": "^1.3.3",
71-
"bun-types": "^1.3.3",
66+
"@typescript-eslint/eslint-plugin": "8.52.0",
67+
"@typescript-eslint/parser": "8.52.0",
68+
"@vitest/coverage-v8": "^4.0.16",
69+
"@vitest/ui": "^4.0.16",
70+
"bun": "^1.3.5",
71+
"bun-types": "^1.3.5",
7272
"dayjs": "^1.11.19",
73-
"drizzle-kit": "^0.31.7",
74-
"eslint": "9.39.1",
73+
"drizzle-kit": "^0.31.8",
74+
"eslint": "9.39.2",
7575
"eslint-config-prettier": "10.1.8",
7676
"eslint-plugin-import": "^2.32.0",
7777
"eslint-plugin-jsonc": "^2.21.0",
@@ -83,13 +83,13 @@
8383
"eslint-plugin-sort-keys-fix": "^1.1.2",
8484
"eslint-plugin-unicorn": "^62.0.0",
8585
"node-cron": "^4.2.1",
86-
"prettier": "^3.7.1",
87-
"tsx": "^4.20.6",
88-
"type-fest": "^5.2.0",
86+
"prettier": "^3.7.4",
87+
"tsx": "^4.21.0",
88+
"type-fest": "^5.3.1",
8989
"typescript": "^5.9.3",
9090
"uuid": "^13.0.0",
91-
"vitest": "^4.0.14",
92-
"ws": "^8.18.3"
91+
"vitest": "^4.0.16",
92+
"ws": "^8.19.0"
9393
},
9494
"packageManager": "yarn@4.12.0"
9595
}

src/helpers/common-config.mts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
PICK_ENTITY,
77
RemoveCallback,
88
TEntityUpdateCallback,
9+
TRawDomains,
910
} from "@digital-alchemy/hass";
1011
import type { CamelCase } from "type-fest";
1112

@@ -181,12 +182,21 @@ export type NonReactive<CONFIGURATION extends object> = {
181182
: CONFIGURATION[KEY];
182183
};
183184

185+
/**
186+
* Extract the domain from a PICK_ENTITY type
187+
*/
188+
type ExtractDomain<ENTITY> = ENTITY extends PICK_ENTITY<infer DOMAIN> ? DOMAIN : TRawDomains;
189+
184190
export type CommonMethods<
185191
CONFIGURATION extends object,
186192
LOCALS extends object,
187193
DATA extends object,
188194
ENTITY extends PICK_ENTITY = PICK_ENTITY,
189195
> = {
196+
/**
197+
* The domain of this entity (e.g., "sensor", "light", "switch")
198+
*/
199+
domain: ExtractDomain<ENTITY>;
190200
/**
191201
* Look up the actual entity_id that is mapped to this entity by unique_id
192202
*/
@@ -295,5 +305,6 @@ export type GenericSynapseEntity<DATA extends object = object> = SynapseEntityPr
295305
TEventMap,
296306
object,
297307
object,
298-
DATA
308+
DATA,
309+
PICK_ENTITY
299310
>;

src/services/generator.service.mts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ export function DomainGeneratorService({ logger, internal, synapse, event, hass
153153
const locals = synapse.locals.localsProxy(unique_id as TSynapseId, entity.locals ?? {});
154154

155155
const keys = is.unique([
156+
"domain",
156157
"locals",
157158
"getEntity",
158159
"storage",
@@ -221,6 +222,11 @@ export function DomainGeneratorService({ logger, internal, synapse, event, hass
221222
: out;
222223
}
223224
switch (property) {
225+
// #MARK: domain
226+
case "domain": {
227+
return domain;
228+
}
229+
224230
// #MARK: locals
225231
case "locals": {
226232
return locals.proxy;

src/test/generator.spec.mts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,138 @@ describe("Generator", () => {
1111
vi.restoreAllMocks();
1212
});
1313

14+
// #MARK: domain property
15+
describe("domain property", () => {
16+
it("returns correct domain for sensor entities", async () => {
17+
expect.assertions(1);
18+
await synapseTestRunner.run(({ synapse, context }) => {
19+
const sensor = synapse.sensor({ context, name: "test" });
20+
expect(sensor.domain).toBe("sensor");
21+
});
22+
});
23+
24+
it("returns correct domain for binary_sensor entities", async () => {
25+
expect.assertions(1);
26+
await synapseTestRunner.run(({ synapse, context }) => {
27+
const binarySensor = synapse.binary_sensor({ context, name: "test" });
28+
expect(binarySensor.domain).toBe("binary_sensor");
29+
});
30+
});
31+
32+
it("returns correct domain for switch entities", async () => {
33+
expect.assertions(1);
34+
await synapseTestRunner.run(({ synapse, context }) => {
35+
const switchEntity = synapse.switch({ context, name: "test" });
36+
expect(switchEntity.domain).toBe("switch");
37+
});
38+
});
39+
40+
it("returns correct domain for button entities", async () => {
41+
expect.assertions(1);
42+
await synapseTestRunner.run(({ synapse, context }) => {
43+
const button = synapse.button({ context, name: "test" });
44+
expect(button.domain).toBe("button");
45+
});
46+
});
47+
48+
it("returns correct domain for lock entities", async () => {
49+
expect.assertions(1);
50+
await synapseTestRunner.run(({ synapse, context }) => {
51+
const lock = synapse.lock({ context, name: "test" });
52+
expect(lock.domain).toBe("lock");
53+
});
54+
});
55+
56+
it("returns correct domain for number entities", async () => {
57+
expect.assertions(1);
58+
await synapseTestRunner.run(({ synapse, context }) => {
59+
const number = synapse.number({ context, name: "test" });
60+
expect(number.domain).toBe("number");
61+
});
62+
});
63+
64+
it("returns correct domain for text entities", async () => {
65+
expect.assertions(1);
66+
await synapseTestRunner.run(({ synapse, context }) => {
67+
const text = synapse.text({ context, name: "test" });
68+
expect(text.domain).toBe("text");
69+
});
70+
});
71+
72+
it("returns correct domain for select entities", async () => {
73+
expect.assertions(1);
74+
await synapseTestRunner.run(({ synapse, context }) => {
75+
const select = synapse.select({ context, name: "test", options: ["a", "b"] });
76+
expect(select.domain).toBe("select");
77+
});
78+
});
79+
80+
it("returns correct domain for scene entities", async () => {
81+
expect.assertions(1);
82+
await synapseTestRunner.run(({ synapse, context }) => {
83+
const scene = synapse.scene({ context, name: "test" });
84+
expect(scene.domain).toBe("scene");
85+
});
86+
});
87+
88+
it("returns correct domain for date entities", async () => {
89+
expect.assertions(1);
90+
await synapseTestRunner.run(({ synapse, context }) => {
91+
const date = synapse.date({ context, name: "test" });
92+
expect(date.domain).toBe("date");
93+
});
94+
});
95+
96+
it("returns correct domain for datetime entities", async () => {
97+
expect.assertions(1);
98+
await synapseTestRunner.run(({ synapse, context }) => {
99+
const datetime = synapse.datetime({ context, name: "test" });
100+
expect(datetime.domain).toBe("datetime");
101+
});
102+
});
103+
104+
it("returns correct domain for time entities", async () => {
105+
expect.assertions(1);
106+
await synapseTestRunner.run(({ synapse, context }) => {
107+
const time = synapse.time({ context, name: "test" });
108+
expect(time.domain).toBe("time");
109+
});
110+
});
111+
112+
it("domain property is included in ownKeys", async () => {
113+
expect.assertions(1);
114+
await synapseTestRunner.run(({ synapse, context }) => {
115+
const sensor = synapse.sensor({ context, name: "test" });
116+
expect(Object.keys(sensor)).toContain("domain");
117+
});
118+
});
119+
120+
it("domain property is accessible via 'in' operator", async () => {
121+
expect.assertions(1);
122+
await synapseTestRunner.run(({ synapse, context }) => {
123+
const sensor = synapse.sensor({ context, name: "test" });
124+
expect("domain" in sensor).toBe(true);
125+
});
126+
});
127+
128+
it("domain property is read-only", async () => {
129+
expect.assertions(1);
130+
await synapseTestRunner.run(({ synapse, context, lifecycle }) => {
131+
lifecycle.onReady(() => {
132+
const sensor = synapse.sensor({ context, name: "test" });
133+
expect(() => {
134+
// @ts-expect-error test
135+
sensor.domain = "other";
136+
}).toThrow();
137+
});
138+
});
139+
});
140+
});
141+
14142
// #MARK: isRegistered
15143
describe("operators", () => {
16144
const SENSOR_KEYS = [
145+
"domain",
17146
"getEntity",
18147
"storage",
19148
"onUpdate",

0 commit comments

Comments
 (0)