Skip to content

Commit a955813

Browse files
authored
feat: add frontend service for theme lookups (#229)
## Summary Adds a new frontend service for retrieving Home Assistant themes, following the same pattern as the addon service. ## Changes - **New FrontendService**: `hass.frontend.getThemes()` method that fetches themes via websocket - **ThemeDefinition interface**: Defines common theme properties including: - Common CSS variables (primary-color, accent-color, divider-color, etc.) - Support for theme modes (light/dark) - Index signature for additional properties - **HassThemeMapping interface**: Type-safe mapping of all available theme names with their supported modes - Themes without modes: `true` - Themes with modes: string union (e.g., `"light" | "dark"`) - **Tests**: Comprehensive test suite following the addon service pattern - **Service registration**: Frontend service registered in `hass.module.mts` ## Type Safety The service returns `Record<keyof HassThemeMapping, ThemeDefinition>`, ensuring type safety for theme names and their definitions. ## Testing - ✅ Tests for successful theme retrieval - ✅ Tests for empty theme responses - ✅ Follows existing addon service test patterns
1 parent 758a66e commit a955813

12 files changed

Lines changed: 163 additions & 4 deletions

cspell.config.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,21 @@ words:
2222
- rgbww
2323
- hacs
2424
- mireds
25+
- gbuttons
2526
- rrule
2627
- rtsp
2728
- sonarjs
29+
- cardassia
30+
- zeldaar
31+
- breen
2832
- mbit
2933
- datetime
3034
- kbit
3135
- postdata
3236
- ssdp
3337
- systype
3438
- tvshow
39+
- lcars
3540
- zeroconf
3641
- templow
3742
- partlycloudy

src/dev/mappings.mts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,4 +163,33 @@ declare module "../user.mts" {
163163
_downstairs: "switch.kitchen_cabinets" | "switch.living_room_mood_lights";
164164
_upstairs: "switch.bedroom_lamp";
165165
}
166+
167+
export interface HassThemeMapping {
168+
"(DO NOT USE/MODIFY)=== LCARS variables": true;
169+
"(DO NOT USE/MODIFY)=== Base customizations": true;
170+
"(DO NOT USE/MODIFY)=== card-mod CSS": true;
171+
"LCARS Default": true;
172+
"LCARS Classic": true;
173+
"LCARS Nemesis Blue": true;
174+
"LCARS Lower Decks I": true;
175+
"LCARS Lower Decks II": true;
176+
"LCARS Romulus": true;
177+
"LCARS Kronos": true;
178+
"LCARS Cardassia": true;
179+
"LCARS Zeldaar": true;
180+
"LCARS Modern": true;
181+
"LCARS Picard I": true;
182+
"LCARS Picard II": true;
183+
"LCARS Red Alert": true;
184+
"LCARS TNG": true;
185+
"LCARS TNGbuttons": true;
186+
"LCARS Transporter": true;
187+
"LCARS Navigation": true;
188+
"LCARS 25C": true;
189+
"LCARS 25C (Red Alert)": true;
190+
"LCARS 25C (Yellow Alert)": true;
191+
"LCARS 25C (Blue Alert)": true;
192+
"LCARS Breen": true;
193+
"Minimal Ninja": "light" | "dark";
194+
}
166195
}

src/hass.module.mts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
FetchAPI,
1313
FetchInternals,
1414
Floor,
15+
FrontendService,
1516
HassDiagnosticsService,
1617
HassFeatureService,
1718
IDByExtension,
@@ -208,6 +209,11 @@ export const LIB_HASS = CreateLibrary({
208209
*/
209210
floor: Floor,
210211

212+
/**
213+
* frontend interactions
214+
*/
215+
frontend: FrontendService,
216+
211217
/**
212218
* search for entity ids in a type safe way
213219
*/

src/helpers/frontend.mts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export interface ThemeDefinition {
2+
"accent-color"?: string;
3+
"app-header-background-color"?: string;
4+
"app-header-edit-background-color"?: string;
5+
"app-header-text-color"?: string;
6+
"card-mod-root-yaml"?: string;
7+
"divider-color"?: string;
8+
"ha-card-border-radius"?: string;
9+
"ha-card-border-width"?: string;
10+
"paper-listbox-background-color"?: string;
11+
"primary-color"?: string;
12+
"sidebar-background-color"?: string;
13+
"sidebar-menu-button-background-color"?: string;
14+
modes?: {
15+
light?: Record<string, unknown>;
16+
dark?: Record<string, unknown>;
17+
};
18+
[key: string]: unknown;
19+
}

src/helpers/index.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export * from "./entity-state.mts";
77
export * from "./features.mts";
88
export * from "./fetch.mts";
99
export * from "./fetch/index.mts";
10+
export * from "./frontend.mts";
1011
export * from "./id-by.mts";
1112
export * from "./interfaces.mts";
1213
export * from "./languages.mts";

src/helpers/interfaces.mts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ import type EventEmitter from "events";
33
import type { EmptyObject } from "type-fest";
44
import type WS from "ws";
55

6-
import type { AddonDetails } from "../index.mts";
6+
import type { AddonDetails, ThemeDefinition } from "../index.mts";
77
import type {
88
ALL_DOMAINS,
99
ANY_ENTITY,
10+
HassThemeMapping,
1011
HassUniqueIdMapping,
1112
iCallService,
1213
PICK_ENTITY,
@@ -54,6 +55,10 @@ export type HassAddonService = {
5455
list: () => Promise<AddonDetails[]>;
5556
};
5657

58+
export type HassFrontendService = {
59+
getThemes: () => Promise<Record<keyof HassThemeMapping, ThemeDefinition>>;
60+
};
61+
5762
export type HassAreaService = {
5863
apply: (
5964
area: TAreaId,

src/services/addon.service.mts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ import type { AddonDetails, HassAddonService } from "../index.mts";
55
export function AddonService({ hass, logger }: TServiceParams): HassAddonService {
66
async function list() {
77
logger.trace({ name: "list" }, "fetching addon list");
8-
return await hass.socket.sendMessage<AddonDetails[]>({
8+
const { addons } = await hass.socket.sendMessage<{ addons: AddonDetails[] }>({
99
endpoint: "/addons",
1010
method: "get",
1111
type: "supervisor/api",
1212
});
13+
return addons;
1314
}
1415

1516
return {

src/services/frontend.service.mts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { TServiceParams } from "@digital-alchemy/core";
2+
3+
import type { HassFrontendService, ThemeDefinition } from "../index.mts";
4+
import type { HassThemeMapping } from "../user.mts";
5+
6+
export function FrontendService({ hass, logger }: TServiceParams): HassFrontendService {
7+
async function getThemes() {
8+
logger.trace({ name: "getThemes" }, "fetching themes");
9+
const result = await hass.socket.sendMessage<{
10+
themes: Record<keyof HassThemeMapping, ThemeDefinition>;
11+
}>({
12+
type: "frontend/get_themes",
13+
});
14+
return result.themes;
15+
}
16+
17+
return {
18+
getThemes,
19+
};
20+
}

src/services/index.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export * from "./events.service.mts";
1010
export * from "./feature.service.mts";
1111
export * from "./fetch-api.service.mts";
1212
export * from "./floor.service.mts";
13+
export * from "./frontend.service.mts";
1314
export * from "./id-by.service.mts";
1415
export * from "./internal.service.mts";
1516
export * from "./label.service.mts";

src/testing/addon.spec.mts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ describe("Addon Service", () => {
3434
];
3535

3636
await hassTestRunner.run(({ lifecycle, hass }) => {
37-
const spy = vi.spyOn(hass.socket, "sendMessage").mockImplementation(async () => mockAddons);
37+
const spy = vi
38+
.spyOn(hass.socket, "sendMessage")
39+
.mockImplementation(async () => ({ addons: mockAddons }));
3840

3941
lifecycle.onReady(async () => {
4042
const result = await hass.addon.list();
@@ -51,7 +53,7 @@ describe("Addon Service", () => {
5153
it("should return empty array when no addons are available", async () => {
5254
expect.assertions(1);
5355
await hassTestRunner.run(({ lifecycle, hass }) => {
54-
vi.spyOn(hass.socket, "sendMessage").mockImplementation(async () => []);
56+
vi.spyOn(hass.socket, "sendMessage").mockImplementation(async () => ({ addons: [] }));
5557

5658
lifecycle.onReady(async () => {
5759
const result = await hass.addon.list();

0 commit comments

Comments
 (0)