Skip to content

Commit d54670e

Browse files
committed
feat(homeassistant): split TRVZB weekly_schedule into per-day sensors
Create individual sensors for each day of the week instead of a single composite sensor with json_attributes. Only applies to SONOFF TRVZB.
1 parent 8a6756a commit d54670e

File tree

2 files changed

+64
-68
lines changed

2 files changed

+64
-68
lines changed

lib/extension/homeassistant.ts

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -291,15 +291,6 @@ const LIST_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
291291
level_config: {entity_category: "diagnostic"},
292292
programming_mode: {icon: "mdi:calendar-clock"},
293293
schedule_settings: {icon: "mdi:calendar-clock"},
294-
schedule: {
295-
icon: "mdi:calendar-clock",
296-
entity_category: "config",
297-
value_template:
298-
"{% set s = value_json.weekly_schedule %}" +
299-
"{% if s %}{{ s.keys() | list | length }} days configured{% else %}Not configured{% endif %}",
300-
json_attributes_topic: true,
301-
json_attributes_template: `{{ {'schedule': value_json.weekly_schedule} | tojson }}`,
302-
},
303294
} as const;
304295

305296
const featurePropertyWithoutEndpoint = (feature: zhc.Feature): string => {
@@ -1199,6 +1190,35 @@ export class HomeAssistant extends Extension {
11991190
case "composite":
12001191
case "list": {
12011192
const firstExposeTyped = firstExpose as zhc.Text | zhc.Composite | zhc.List;
1193+
1194+
// Special handling for SONOFF TRVZB weekly schedule: create individual sensors per day
1195+
if (
1196+
firstExposeTyped.type === "composite" &&
1197+
firstExposeTyped.name === "schedule" &&
1198+
firstExposeTyped.property === "weekly_schedule" &&
1199+
definition?.vendor === "SONOFF" &&
1200+
definition?.model === "TRVZB"
1201+
) {
1202+
const compositeExpose = firstExposeTyped as zhc.Composite;
1203+
for (const feature of compositeExpose.features) {
1204+
if (feature.type === "text" && feature.access & ACCESS_STATE) {
1205+
const dayName = feature.name.charAt(0).toUpperCase() + feature.name.slice(1);
1206+
discoveryEntries.push({
1207+
type: "sensor",
1208+
object_id: `${compositeExpose.property}_${feature.property}`,
1209+
mockProperties: [{property: compositeExpose.property, value: null}],
1210+
discovery_payload: {
1211+
name: endpoint ? `Schedule ${dayName} ${endpoint}` : `Schedule ${dayName}`,
1212+
value_template: `{{ value_json.${compositeExpose.property}.${feature.property} | default('', True) }}`,
1213+
icon: "mdi:calendar-clock",
1214+
entity_category: "diagnostic",
1215+
},
1216+
});
1217+
}
1218+
}
1219+
break;
1220+
}
1221+
12021222
if (firstExposeTyped.type === "text" && firstExposeTyped.access & ACCESS_SET) {
12031223
discoveryEntries.push({
12041224
type: "text",

test/extensions/homeassistant.test.ts

Lines changed: 35 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ describe("Extension: HomeAssistant", () => {
9393
await flushPromises();
9494
});
9595

96-
it("Should discover weekly_schedule sensor with json_attributes instead of truncated value", () => {
96+
it("Should discover SONOFF TRVZB weekly_schedule as individual sensors per day", () => {
9797
// Create a SONOFF TRVZB expose definition with schedule (composite type)
9898
const trvzbExposes = [
9999
{
@@ -145,90 +145,66 @@ describe("Extension: HomeAssistant", () => {
145145

146146
// @ts-expect-error private
147147
const configs = extension.getConfigs(mockDevice);
148-
const weeklyScheduleConfig = configs.find((c) => c.object_id === "weekly_schedule");
149148

150-
expect(weeklyScheduleConfig).toBeDefined();
151-
expect(weeklyScheduleConfig!.discovery_payload.icon).toBe("mdi:calendar-clock");
152-
// Note: entity_category is converted from "config" to "diagnostic" for sensors in HA
153-
expect(weeklyScheduleConfig!.discovery_payload.entity_category).toBe("diagnostic");
154-
155-
// Verify value_template shows a summary, not the raw JSON
156-
expect(weeklyScheduleConfig!.discovery_payload.value_template).toContain("days configured");
157-
expect(weeklyScheduleConfig!.discovery_payload.value_template).not.toContain("truncate");
158-
159-
// Verify json_attributes are used
160-
expect(weeklyScheduleConfig!.discovery_payload.json_attributes_topic).toBeDefined();
161-
expect(weeklyScheduleConfig!.discovery_payload.json_attributes_template).toBeDefined();
162-
expect(weeklyScheduleConfig!.discovery_payload.json_attributes_template).toContain("schedule");
149+
// Should NOT have a single weekly_schedule sensor
150+
const singleScheduleConfig = configs.find((c) => c.object_id === "weekly_schedule");
151+
expect(singleScheduleConfig).toBeUndefined();
152+
153+
// Should have 7 individual day sensors
154+
const days = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
155+
for (const day of days) {
156+
const dayConfig = configs.find((c) => c.object_id === `weekly_schedule_${day}`);
157+
expect(dayConfig).toBeDefined();
158+
expect(dayConfig!.type).toBe("sensor");
159+
expect(dayConfig!.discovery_payload.icon).toBe("mdi:calendar-clock");
160+
expect(dayConfig!.discovery_payload.entity_category).toBe("diagnostic");
161+
expect(dayConfig!.discovery_payload.name).toBe(`Schedule ${day.charAt(0).toUpperCase() + day.slice(1)}`);
162+
expect(dayConfig!.discovery_payload.value_template).toBe(`{{ value_json.weekly_schedule.${day} | default('', True) }}`);
163+
// Should NOT use json_attributes
164+
expect(dayConfig!.discovery_payload.json_attributes_topic).toBeUndefined();
165+
expect(dayConfig!.discovery_payload.json_attributes_template).toBeUndefined();
166+
}
163167
});
164168

165-
it("Should discover SONOFF TRVZB schedule sensor with json_attributes", () => {
166-
// Create a SONOFF TRVZB expose definition with schedule (composite type)
167-
const trvzbExposes = [
168-
{
169-
type: "climate",
170-
features: [
171-
{
172-
name: "occupied_heating_setpoint",
173-
property: "occupied_heating_setpoint",
174-
type: "numeric",
175-
access: 7,
176-
value_min: 4,
177-
value_max: 35,
178-
value_step: 0.5,
179-
},
180-
{name: "local_temperature", property: "local_temperature", type: "numeric", access: 5},
181-
{name: "system_mode", property: "system_mode", type: "enum", access: 7, values: ["off", "auto", "heat"]},
182-
{name: "running_state", property: "running_state", type: "enum", access: 5, values: ["idle", "heat"]},
183-
],
184-
},
169+
it("Should NOT apply TRVZB schedule handling to other devices", () => {
170+
// Create a non-TRVZB device with similar schedule expose
171+
const otherDeviceExposes = [
185172
{
186173
type: "composite",
187174
name: "schedule",
188175
property: "weekly_schedule",
189176
label: "Schedule",
190-
access: 3,
177+
access: 1, // Only STATE access
191178
category: "config",
192179
features: [
193-
{name: "sunday", property: "sunday", type: "text", access: 3},
194-
{name: "monday", property: "monday", type: "text", access: 3},
195-
{name: "tuesday", property: "tuesday", type: "text", access: 3},
196-
{name: "wednesday", property: "wednesday", type: "text", access: 3},
197-
{name: "thursday", property: "thursday", type: "text", access: 3},
198-
{name: "friday", property: "friday", type: "text", access: 3},
199-
{name: "saturday", property: "saturday", type: "text", access: 3},
180+
{name: "sunday", property: "sunday", type: "text", access: 1},
181+
{name: "monday", property: "monday", type: "text", access: 1},
200182
],
201183
},
202184
];
203185

204-
// Create a mock device with TRVZB exposes
186+
// Create a mock device with different vendor/model
205187
const mockDevice = {
206-
definition: {vendor: "SONOFF", model: "TRVZB"},
188+
definition: {vendor: "OTHER_VENDOR", model: "OTHER_MODEL"},
207189
isDevice: () => true,
208190
isGroup: () => false,
209191
options: {ID: "0x1234567890abcdef"},
210-
exposes: () => trvzbExposes,
192+
exposes: () => otherDeviceExposes,
211193
zh: {endpoints: []},
212-
name: "test_trvzb",
194+
name: "test_other",
213195
};
214196

215197
// @ts-expect-error private method
216198
const configs = extension.getConfigs(mockDevice);
217199

218-
// Find the schedule sensor config
200+
// Should have a single weekly_schedule sensor (default behavior)
219201
const scheduleConfig = configs.find((c) => c.object_id === "weekly_schedule");
220-
221202
expect(scheduleConfig).toBeDefined();
222-
expect(scheduleConfig.type).toBe("sensor");
223-
expect(scheduleConfig.discovery_payload.icon).toBe("mdi:calendar-clock");
224-
expect(scheduleConfig.discovery_payload.value_template).toContain("days configured");
225-
expect(scheduleConfig.discovery_payload.value_template).not.toContain("truncate");
226-
227-
// Most importantly: verify json_attributes_topic is set to true (will be converted to actual topic)
228-
expect(scheduleConfig.discovery_payload.json_attributes_topic).toBe(true);
229-
expect(scheduleConfig.discovery_payload.json_attributes_template).toBeDefined();
230-
expect(scheduleConfig.discovery_payload.json_attributes_template).toContain("schedule");
231-
expect(scheduleConfig.discovery_payload.json_attributes_template).toContain("weekly_schedule");
203+
expect(scheduleConfig!.type).toBe("sensor");
204+
205+
// Should NOT have individual day sensors
206+
const sundayConfig = configs.find((c) => c.object_id === "weekly_schedule_sunday");
207+
expect(sundayConfig).toBeUndefined();
232208
});
233209

234210
it("Should not have duplicate type/object_ids in a mapping", async () => {

0 commit comments

Comments
 (0)