Skip to content

Commit 8c60d57

Browse files
gheylenclaude
andauthored
Feat: expose door and window combined state as Homey capabilities (#114)
* Feat: expose door and window combined state as capabilities Adds door_state_capability and window_state_capability for all vehicles. The transformer already populated these fields from real CarData API keys; this wires them into the Homey device and migrates existing devices. Also fixes a stale test assertion that expected RANGE_BATTERY on a pure BEV — the code has always reserved it for PHEVs only. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Feat: add door and window capability definitions to homeycompose Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Style: sync app.json with homey app build output (alphabetical capability order) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 15749dc commit 8c60d57

7 files changed

Lines changed: 206 additions & 7 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"type": "enum",
3+
"title": {
4+
"en": "Doors",
5+
"nl": "Deuren"
6+
},
7+
"values": [
8+
{ "id": "OPEN", "title": { "en": "Open", "nl": "Open" } },
9+
{ "id": "CLOSED", "title": { "en": "Closed", "nl": "Gesloten" } },
10+
{ "id": "UNKNOWN", "title": { "en": "Unknown", "nl": "Onbekend" } }
11+
],
12+
"getable": true,
13+
"setable": false,
14+
"uiComponent": "sensor"
15+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"type": "enum",
3+
"title": {
4+
"en": "Windows",
5+
"nl": "Ramen"
6+
},
7+
"values": [
8+
{ "id": "OPEN", "title": { "en": "Open", "nl": "Open" } },
9+
{ "id": "CLOSED", "title": { "en": "Closed", "nl": "Gesloten" } },
10+
{ "id": "INTERMEDIATE", "title": { "en": "Intermediate", "nl": "Tussenstand" } },
11+
{ "id": "UNKNOWN", "title": { "en": "Unknown", "nl": "Onbekend" } }
12+
],
13+
"getable": true,
14+
"setable": false,
15+
"uiComponent": "sensor"
16+
}

app.json

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1133,6 +1133,39 @@
11331133
"setable": false,
11341134
"uiComponent": "sensor"
11351135
},
1136+
"door_state_capability": {
1137+
"type": "enum",
1138+
"title": {
1139+
"en": "Doors",
1140+
"nl": "Deuren"
1141+
},
1142+
"values": [
1143+
{
1144+
"id": "OPEN",
1145+
"title": {
1146+
"en": "Open",
1147+
"nl": "Open"
1148+
}
1149+
},
1150+
{
1151+
"id": "CLOSED",
1152+
"title": {
1153+
"en": "Closed",
1154+
"nl": "Gesloten"
1155+
}
1156+
},
1157+
{
1158+
"id": "UNKNOWN",
1159+
"title": {
1160+
"en": "Unknown",
1161+
"nl": "Onbekend"
1162+
}
1163+
}
1164+
],
1165+
"getable": true,
1166+
"setable": false,
1167+
"uiComponent": "sensor"
1168+
},
11361169
"mileage_capability": {
11371170
"type": "number",
11381171
"title": {
@@ -1171,6 +1204,46 @@
11711204
"icon": "/assets/gas-station.svg",
11721205
"units": "l",
11731206
"insights": true
1207+
},
1208+
"window_state_capability": {
1209+
"type": "enum",
1210+
"title": {
1211+
"en": "Windows",
1212+
"nl": "Ramen"
1213+
},
1214+
"values": [
1215+
{
1216+
"id": "OPEN",
1217+
"title": {
1218+
"en": "Open",
1219+
"nl": "Open"
1220+
}
1221+
},
1222+
{
1223+
"id": "CLOSED",
1224+
"title": {
1225+
"en": "Closed",
1226+
"nl": "Gesloten"
1227+
}
1228+
},
1229+
{
1230+
"id": "INTERMEDIATE",
1231+
"title": {
1232+
"en": "Intermediate",
1233+
"nl": "Tussenstand"
1234+
}
1235+
},
1236+
{
1237+
"id": "UNKNOWN",
1238+
"title": {
1239+
"en": "Unknown",
1240+
"nl": "Onbekend"
1241+
}
1242+
}
1243+
],
1244+
"getable": true,
1245+
"setable": false,
1246+
"uiComponent": "sensor"
11741247
}
11751248
}
11761249
}

drivers/Vehicle.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -485,16 +485,15 @@ export class Vehicle extends Device {
485485
}
486486
}
487487

488-
// TODO: Add door and window capabilities to Capabilities.ts and app.json
489488
// Update doors
490-
// if (status.doors) {
491-
// await this.updateCapabilityValue('door_state_capability', status.doors.combinedState);
492-
// }
489+
if (status.doors) {
490+
await this.setCapabilityValueSafe(Capabilities.DOOR_STATE, status.doors.combinedState);
491+
}
493492

494493
// Update windows
495-
// if (status.windows) {
496-
// await this.updateCapabilityValue('window_state_capability', status.windows.combinedState);
497-
// }
494+
if (status.windows) {
495+
await this.setCapabilityValueSafe(Capabilities.WINDOW_STATE, status.windows.combinedState);
496+
}
498497

499498
// Update lock state (read-only status, not control)
500499
if (status.lockState) {
@@ -687,6 +686,10 @@ export class Vehicle extends Device {
687686
await this.removeCapabilitySafe(Capabilities.RANGE);
688687
}
689688

689+
// Door and window state capabilities are available for all vehicles
690+
await this.addCapabilitySafe(Capabilities.DOOR_STATE);
691+
await this.addCapabilitySafe(Capabilities.WINDOW_STATE);
692+
690693
// Add EV charging state capability for Homey v12.4.5+
691694
if (semver.gte(this.homey.version, '12.4.5') && hasElectricDriveTrain) {
692695
await this.addCapabilitySafe(Capabilities.EV_CHARGING_STATE);

tests/drivers/Vehicle.capabilityMigration.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,28 @@ describe('Vehicle Capability Migration Tests', () => {
275275
});
276276
});
277277

278+
describe('Door and Window Capabilities', () => {
279+
it('should_addDoorAndWindowCapabilities_when_electricDrivetrain', async () => {
280+
const mockStateManager = vehicle['stateManager'] as any;
281+
mockStateManager.getDriveTrain.mockReturnValue(DriveTrainType.ELECTRIC);
282+
283+
await vehicle['migrate_device_capabilities']();
284+
285+
expect(addCapabilitySafeSpy).toHaveBeenCalledWith(Capabilities.DOOR_STATE);
286+
expect(addCapabilitySafeSpy).toHaveBeenCalledWith(Capabilities.WINDOW_STATE);
287+
});
288+
289+
it('should_addDoorAndWindowCapabilities_when_combustionDrivetrain', async () => {
290+
const mockStateManager = vehicle['stateManager'] as any;
291+
mockStateManager.getDriveTrain.mockReturnValue(DriveTrainType.COMBUSTION);
292+
293+
await vehicle['migrate_device_capabilities']();
294+
295+
expect(addCapabilitySafeSpy).toHaveBeenCalledWith(Capabilities.DOOR_STATE);
296+
expect(addCapabilitySafeSpy).toHaveBeenCalledWith(Capabilities.WINDOW_STATE);
297+
});
298+
});
299+
278300
describe('Energy Capabilities for Electric Vehicles', () => {
279301
it('should_setEnergyCapabilities_when_electricVehicle', async () => {
280302
// Arrange

tests/drivers/Vehicle.updateCapabilities.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,70 @@ describe('Vehicle.updateCapabilitiesFromStatus', () => {
9393
jest.restoreAllMocks();
9494
});
9595

96+
describe('Door and Window State', () => {
97+
it('should_updateDoorState_when_doorsPresent', async () => {
98+
const status: VehicleStatus = {
99+
vin: 'WBA12345678901234',
100+
driveTrain: DriveTrainType.ELECTRIC,
101+
lastUpdatedAt: new Date(),
102+
doors: {
103+
combinedState: 'OPEN',
104+
leftFront: 'OPEN',
105+
rightFront: 'CLOSED',
106+
leftRear: 'CLOSED',
107+
rightRear: 'CLOSED',
108+
hood: 'CLOSED',
109+
trunk: 'CLOSED',
110+
},
111+
};
112+
113+
await (vehicle as any).updateCapabilitiesFromStatus(status);
114+
115+
expect(setCapabilityValueSafeSpy).toHaveBeenCalledWith(Capabilities.DOOR_STATE, 'OPEN');
116+
});
117+
118+
it('should_updateWindowState_when_windowsPresent', async () => {
119+
const status: VehicleStatus = {
120+
vin: 'WBA12345678901234',
121+
driveTrain: DriveTrainType.ELECTRIC,
122+
lastUpdatedAt: new Date(),
123+
windows: {
124+
combinedState: 'INTERMEDIATE',
125+
leftFront: 'INTERMEDIATE',
126+
rightFront: 'CLOSED',
127+
leftRear: 'CLOSED',
128+
rightRear: 'CLOSED',
129+
},
130+
};
131+
132+
await (vehicle as any).updateCapabilitiesFromStatus(status);
133+
134+
expect(setCapabilityValueSafeSpy).toHaveBeenCalledWith(
135+
Capabilities.WINDOW_STATE,
136+
'INTERMEDIATE'
137+
);
138+
});
139+
140+
it('should_notUpdateDoorOrWindowState_when_bothAbsent', async () => {
141+
const status: VehicleStatus = {
142+
vin: 'WBA12345678901234',
143+
driveTrain: DriveTrainType.ELECTRIC,
144+
lastUpdatedAt: new Date(),
145+
};
146+
147+
await (vehicle as any).updateCapabilitiesFromStatus(status);
148+
149+
expect(setCapabilityValueSafeSpy).not.toHaveBeenCalledWith(
150+
Capabilities.DOOR_STATE,
151+
expect.anything()
152+
);
153+
expect(setCapabilityValueSafeSpy).not.toHaveBeenCalledWith(
154+
Capabilities.WINDOW_STATE,
155+
expect.anything()
156+
);
157+
});
158+
});
159+
96160
describe('P0: Undefined Handling', () => {
97161
it('should_updateAllCapabilities_when_validStatusProvided', async () => {
98162
// Arrange

utils/Capabilities.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ export class Capabilities {
3838
// Vehicle information capabilities
3939
public static readonly MILEAGE = 'mileage_capability';
4040

41+
// Door and window state capabilities
42+
public static readonly DOOR_STATE = 'door_state_capability';
43+
public static readonly WINDOW_STATE = 'window_state_capability';
44+
4145
// Climate capabilities
4246
public static readonly CLIMATE_NOW = 'climate_now_capability';
4347

@@ -62,6 +66,8 @@ export class Capabilities {
6266
Capabilities.LOCATION,
6367
Capabilities.ADDRESS,
6468
Capabilities.MILEAGE,
69+
Capabilities.DOOR_STATE,
70+
Capabilities.WINDOW_STATE,
6571
Capabilities.CLIMATE_NOW,
6672
];
6773

0 commit comments

Comments
 (0)