@riddix/hamh 2.1.0-alpha.855 → 2.1.0-alpha.857
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/dist/backend/cli.js
CHANGED
|
@@ -133515,7 +133515,8 @@ WARNING: ${includeIdentity ? "This backup contains sensitive Matter identity dat
|
|
|
133515
133515
|
fanRestoreSpeedOnPowerOn: config8.fanRestoreSpeedOnPowerOn,
|
|
133516
133516
|
composedEntities: config8.composedEntities,
|
|
133517
133517
|
disableMomentaryFlip: config8.disableMomentaryFlip,
|
|
133518
|
-
vacuumAscendingRoomOrder: config8.vacuumAscendingRoomOrder
|
|
133518
|
+
vacuumAscendingRoomOrder: config8.vacuumAscendingRoomOrder,
|
|
133519
|
+
vacuumRoomSwitches: config8.vacuumRoomSwitches
|
|
133519
133520
|
});
|
|
133520
133521
|
mappingsRestored++;
|
|
133521
133522
|
}
|
|
@@ -133681,7 +133682,8 @@ WARNING: ${includeIdentity ? "This backup contains sensitive Matter identity dat
|
|
|
133681
133682
|
fanWindPresets: config8.fanWindPresets,
|
|
133682
133683
|
composedEntities: config8.composedEntities,
|
|
133683
133684
|
disableMomentaryFlip: config8.disableMomentaryFlip,
|
|
133684
|
-
vacuumAscendingRoomOrder: config8.vacuumAscendingRoomOrder
|
|
133685
|
+
vacuumAscendingRoomOrder: config8.vacuumAscendingRoomOrder,
|
|
133686
|
+
vacuumRoomSwitches: config8.vacuumRoomSwitches
|
|
133685
133687
|
});
|
|
133686
133688
|
mappingsRestored++;
|
|
133687
133689
|
}
|
|
@@ -134533,6 +134535,7 @@ function entityMappingApi(mappingStorage, identityStorage) {
|
|
|
134533
134535
|
currentRoomEntity: body.currentRoomEntity,
|
|
134534
134536
|
cleanedAreaEntity: body.cleanedAreaEntity,
|
|
134535
134537
|
vacuumAscendingRoomOrder: body.vacuumAscendingRoomOrder,
|
|
134538
|
+
vacuumRoomSwitches: body.vacuumRoomSwitches,
|
|
134536
134539
|
disableCustomAreaRoomModes: body.disableCustomAreaRoomModes,
|
|
134537
134540
|
valetudoIdentifier: body.valetudoIdentifier,
|
|
134538
134541
|
coverSwapOpenClose: body.coverSwapOpenClose,
|
|
@@ -135045,7 +135048,8 @@ function configToProfileEntry(config8) {
|
|
|
135045
135048
|
climateExposeFan: config8.climateExposeFan,
|
|
135046
135049
|
climateAutoMode: config8.climateAutoMode,
|
|
135047
135050
|
disableMomentaryFlip: config8.disableMomentaryFlip,
|
|
135048
|
-
vacuumAscendingRoomOrder: config8.vacuumAscendingRoomOrder
|
|
135051
|
+
vacuumAscendingRoomOrder: config8.vacuumAscendingRoomOrder,
|
|
135052
|
+
vacuumRoomSwitches: config8.vacuumRoomSwitches
|
|
135049
135053
|
};
|
|
135050
135054
|
}
|
|
135051
135055
|
function mappingProfileApi(mappingStorage) {
|
|
@@ -135189,7 +135193,8 @@ function mappingProfileApi(mappingStorage) {
|
|
|
135189
135193
|
climateExposeFan: entry.climateExposeFan,
|
|
135190
135194
|
climateAutoMode: entry.climateAutoMode,
|
|
135191
135195
|
disableMomentaryFlip: entry.disableMomentaryFlip,
|
|
135192
|
-
vacuumAscendingRoomOrder: entry.vacuumAscendingRoomOrder
|
|
135196
|
+
vacuumAscendingRoomOrder: entry.vacuumAscendingRoomOrder,
|
|
135197
|
+
vacuumRoomSwitches: entry.vacuumRoomSwitches
|
|
135193
135198
|
});
|
|
135194
135199
|
applied++;
|
|
135195
135200
|
} catch (e) {
|
|
@@ -136588,7 +136593,7 @@ import * as nodePath from "node:path";
|
|
|
136588
136593
|
import express14 from "express";
|
|
136589
136594
|
|
|
136590
136595
|
// src/plugins/builtin/names.ts
|
|
136591
|
-
var BUILTIN_PLUGIN_NAMES = ["camera"];
|
|
136596
|
+
var BUILTIN_PLUGIN_NAMES = ["camera", "security"];
|
|
136592
136597
|
|
|
136593
136598
|
// src/plugins/plugin-installer.ts
|
|
136594
136599
|
init_esm();
|
|
@@ -139966,6 +139971,7 @@ var EntityMappingStorage = class extends Service {
|
|
|
139966
139971
|
currentRoomEntity: request.currentRoomEntity?.trim() || void 0,
|
|
139967
139972
|
cleanedAreaEntity: request.cleanedAreaEntity?.trim() || void 0,
|
|
139968
139973
|
vacuumAscendingRoomOrder: request.vacuumAscendingRoomOrder || void 0,
|
|
139974
|
+
vacuumRoomSwitches: request.vacuumRoomSwitches || void 0,
|
|
139969
139975
|
disableCustomAreaRoomModes: request.disableCustomAreaRoomModes || void 0,
|
|
139970
139976
|
valetudoIdentifier: request.valetudoIdentifier?.trim() || void 0,
|
|
139971
139977
|
coverSwapOpenClose: request.coverSwapOpenClose || void 0,
|
|
@@ -139983,7 +139989,7 @@ var EntityMappingStorage = class extends Service {
|
|
|
139983
139989
|
composedEntities: request.composedEntities?.filter((e) => e.entityId?.trim()) ?? void 0,
|
|
139984
139990
|
disableMomentaryFlip: request.disableMomentaryFlip || void 0
|
|
139985
139991
|
};
|
|
139986
|
-
if (!config8.matterDeviceType && !config8.customName && !config8.customProductName && !config8.customVendorName && !config8.customSerialNumber && config8.customVendorId === void 0 && config8.disabled !== true && !config8.filterLifeEntity && !config8.cleaningModeEntity && !config8.temperatureEntity && !config8.humidityEntity && !config8.batteryEntity && !config8.disableBatteryMapping && !config8.chargingStateEntity && !config8.roomEntities && !config8.disableLockPin && !config8.lockUsercodeService && config8.lockUsercodeSlot === void 0 && config8.lockPinMinLength === void 0 && config8.lockPinMaxLength === void 0 && !config8.powerEntity && !config8.energyEntity && !config8.meterSerialNumber && !config8.pointOfDelivery && !config8.voltageEntity && !config8.currentEntity && !config8.batteryPowerEntity && !config8.batteryEnergyEntity && !config8.chargingSwitchEntity && !config8.currentLimitEntity && !config8.pressureEntity && !config8.suctionLevelEntity && !config8.mopIntensityEntity && (!config8.customServiceAreas || config8.customServiceAreas.length === 0) && (!config8.customFanSpeedTags || Object.keys(config8.customFanSpeedTags).length === 0) && (!config8.fanWindPresets || (config8.fanWindPresets.natural?.length ?? 0) === 0 && (config8.fanWindPresets.sleep?.length ?? 0) === 0) && !config8.fanRestoreSpeedOnPowerOn && !config8.currentRoomEntity && !config8.cleanedAreaEntity && !config8.vacuumAscendingRoomOrder && !config8.disableCustomAreaRoomModes && !config8.valetudoIdentifier && !config8.coverSwapOpenClose && !config8.coverExposeAsDimmableLight && !config8.selectExposeAsSwitch && !config8.selectSwitchOnOption && !config8.selectSwitchOffOption && !config8.coverSliderDebounceMs && !config8.updateThrottleMs && !config8.disableClimateOnOff && !config8.disableClimateFanControl && !config8.climateKeepModeOnIdle && !config8.climateExposeFan && !config8.climateAutoMode && (!config8.composedEntities || config8.composedEntities.length === 0) && !config8.disableMomentaryFlip) {
|
|
139992
|
+
if (!config8.matterDeviceType && !config8.customName && !config8.customProductName && !config8.customVendorName && !config8.customSerialNumber && config8.customVendorId === void 0 && config8.disabled !== true && !config8.filterLifeEntity && !config8.cleaningModeEntity && !config8.temperatureEntity && !config8.humidityEntity && !config8.batteryEntity && !config8.disableBatteryMapping && !config8.chargingStateEntity && !config8.roomEntities && !config8.disableLockPin && !config8.lockUsercodeService && config8.lockUsercodeSlot === void 0 && config8.lockPinMinLength === void 0 && config8.lockPinMaxLength === void 0 && !config8.powerEntity && !config8.energyEntity && !config8.meterSerialNumber && !config8.pointOfDelivery && !config8.voltageEntity && !config8.currentEntity && !config8.batteryPowerEntity && !config8.batteryEnergyEntity && !config8.chargingSwitchEntity && !config8.currentLimitEntity && !config8.pressureEntity && !config8.suctionLevelEntity && !config8.mopIntensityEntity && (!config8.customServiceAreas || config8.customServiceAreas.length === 0) && (!config8.customFanSpeedTags || Object.keys(config8.customFanSpeedTags).length === 0) && (!config8.fanWindPresets || (config8.fanWindPresets.natural?.length ?? 0) === 0 && (config8.fanWindPresets.sleep?.length ?? 0) === 0) && !config8.fanRestoreSpeedOnPowerOn && !config8.currentRoomEntity && !config8.cleanedAreaEntity && !config8.vacuumAscendingRoomOrder && !config8.vacuumRoomSwitches && !config8.disableCustomAreaRoomModes && !config8.valetudoIdentifier && !config8.coverSwapOpenClose && !config8.coverExposeAsDimmableLight && !config8.selectExposeAsSwitch && !config8.selectSwitchOnOption && !config8.selectSwitchOffOption && !config8.coverSliderDebounceMs && !config8.updateThrottleMs && !config8.disableClimateOnOff && !config8.disableClimateFanControl && !config8.climateKeepModeOnIdle && !config8.climateExposeFan && !config8.climateAutoMode && (!config8.composedEntities || config8.composedEntities.length === 0) && !config8.disableMomentaryFlip) {
|
|
139987
139993
|
bridgeMap.delete(request.entityId);
|
|
139988
139994
|
} else {
|
|
139989
139995
|
bridgeMap.set(request.entityId, config8);
|
|
@@ -171139,6 +171145,31 @@ function createCleanAreaServiceAreaServer(cleanAreaRooms) {
|
|
|
171139
171145
|
currentArea: null
|
|
171140
171146
|
});
|
|
171141
171147
|
}
|
|
171148
|
+
function getVacuumServiceAreas(attributes9, mapping) {
|
|
171149
|
+
const cleanAreaRooms = mapping?.cleanAreaRooms;
|
|
171150
|
+
if (cleanAreaRooms && cleanAreaRooms.length > 0) {
|
|
171151
|
+
return cleanAreaRooms.map((room) => ({
|
|
171152
|
+
areaId: room.areaId,
|
|
171153
|
+
name: room.name
|
|
171154
|
+
}));
|
|
171155
|
+
}
|
|
171156
|
+
const customAreas = mapping?.customServiceAreas;
|
|
171157
|
+
if (customAreas && customAreas.length > 0) {
|
|
171158
|
+
return customAreas.map((area, index) => ({
|
|
171159
|
+
areaId: index + 1,
|
|
171160
|
+
name: area.name
|
|
171161
|
+
}));
|
|
171162
|
+
}
|
|
171163
|
+
const roomEntities = mapping?.roomEntities;
|
|
171164
|
+
const rooms = roomEntities && roomEntities.length > 0 ? buttonEntitiesToRooms(roomEntities, attributes9) : parseVacuumRooms(attributes9);
|
|
171165
|
+
if (rooms.length > 0) {
|
|
171166
|
+
return roomsToAreas(rooms).map((area) => ({
|
|
171167
|
+
areaId: area.areaId,
|
|
171168
|
+
name: area.areaInfo.locationInfo?.locationName ?? ""
|
|
171169
|
+
}));
|
|
171170
|
+
}
|
|
171171
|
+
return [];
|
|
171172
|
+
}
|
|
171142
171173
|
|
|
171143
171174
|
// src/matter/endpoints/legacy/vacuum/behaviors/vacuum-rvc-run-mode-server.ts
|
|
171144
171175
|
var logger236 = Logger.get("VacuumRvcRunModeServer");
|
|
@@ -171313,6 +171344,135 @@ var cleaningStates = [
|
|
|
171313
171344
|
function vacuumIsCleaning(state) {
|
|
171314
171345
|
return state != null && cleaningStates.includes(state);
|
|
171315
171346
|
}
|
|
171347
|
+
function dispatchRoomClean(attributes9, mapping, entityId, selectedAreas, seam) {
|
|
171348
|
+
const customAreas = mapping?.customServiceAreas;
|
|
171349
|
+
if (customAreas && customAreas.length > 0) {
|
|
171350
|
+
const ephemeral = {
|
|
171351
|
+
completedAreas: /* @__PURE__ */ new Set(),
|
|
171352
|
+
lastCurrentArea: null,
|
|
171353
|
+
activeAreas: [],
|
|
171354
|
+
loggedShortCircuits: /* @__PURE__ */ new Set(),
|
|
171355
|
+
observedCleaning: false,
|
|
171356
|
+
pendingDispatches: [],
|
|
171357
|
+
cleanedAreaBaseline: null
|
|
171358
|
+
};
|
|
171359
|
+
const action = handleCustomServiceAreas(
|
|
171360
|
+
selectedAreas,
|
|
171361
|
+
customAreas,
|
|
171362
|
+
ephemeral
|
|
171363
|
+
);
|
|
171364
|
+
return { action, pending: ephemeral.pendingDispatches };
|
|
171365
|
+
}
|
|
171366
|
+
const cleanAreaRooms = mapping?.cleanAreaRooms;
|
|
171367
|
+
if (cleanAreaRooms && cleanAreaRooms.length > 0) {
|
|
171368
|
+
const haAreaIds = resolveCleanAreaIds(selectedAreas, cleanAreaRooms);
|
|
171369
|
+
if (haAreaIds.length > 0) {
|
|
171370
|
+
logger236.info(`CLEAN_AREA: cleaning HA areas: ${haAreaIds.join(", ")}`);
|
|
171371
|
+
return {
|
|
171372
|
+
action: {
|
|
171373
|
+
action: "vacuum.clean_area",
|
|
171374
|
+
data: { cleaning_area_id: haAreaIds }
|
|
171375
|
+
},
|
|
171376
|
+
pending: []
|
|
171377
|
+
};
|
|
171378
|
+
}
|
|
171379
|
+
}
|
|
171380
|
+
const roomEntities = mapping?.roomEntities;
|
|
171381
|
+
if (roomEntities && roomEntities.length > 0) {
|
|
171382
|
+
const matched = [];
|
|
171383
|
+
for (const areaId of selectedAreas) {
|
|
171384
|
+
const buttonId = roomEntities.find((id) => toAreaId(id) === areaId);
|
|
171385
|
+
if (buttonId) {
|
|
171386
|
+
matched.push({ areaId, entityId: buttonId });
|
|
171387
|
+
}
|
|
171388
|
+
}
|
|
171389
|
+
if (matched.length > 0) {
|
|
171390
|
+
logger236.info(
|
|
171391
|
+
`Roborock: ${matched.length} room button(s) queued: ${matched.map((m) => m.entityId).join(", ")}`
|
|
171392
|
+
);
|
|
171393
|
+
return {
|
|
171394
|
+
action: { action: "button.press", target: matched[0].entityId },
|
|
171395
|
+
pending: matched.slice(1).map(({ areaId, entityId: buttonId }) => ({
|
|
171396
|
+
areaId,
|
|
171397
|
+
action: { action: "button.press", target: buttonId }
|
|
171398
|
+
}))
|
|
171399
|
+
};
|
|
171400
|
+
}
|
|
171401
|
+
}
|
|
171402
|
+
if (entityId.startsWith("vacuum.valetudo_")) {
|
|
171403
|
+
return {
|
|
171404
|
+
action: buildValetudoSegmentAction(
|
|
171405
|
+
entityId,
|
|
171406
|
+
selectedAreas,
|
|
171407
|
+
mapping?.valetudoIdentifier
|
|
171408
|
+
),
|
|
171409
|
+
pending: []
|
|
171410
|
+
};
|
|
171411
|
+
}
|
|
171412
|
+
const rooms = parseVacuumRooms(attributes9);
|
|
171413
|
+
const roomIds = [];
|
|
171414
|
+
let targetMapName;
|
|
171415
|
+
for (const areaId of selectedAreas) {
|
|
171416
|
+
const room = rooms.find((r) => toAreaId(r.id) === areaId);
|
|
171417
|
+
if (room) {
|
|
171418
|
+
roomIds.push(room.originalId ?? room.id);
|
|
171419
|
+
if (room.mapName && !targetMapName) {
|
|
171420
|
+
targetMapName = room.mapName;
|
|
171421
|
+
}
|
|
171422
|
+
}
|
|
171423
|
+
}
|
|
171424
|
+
if (roomIds.length > 0) {
|
|
171425
|
+
logger236.info(`Starting cleaning with selected areas: ${roomIds.join(", ")}`);
|
|
171426
|
+
if (isDreameVacuum(attributes9)) {
|
|
171427
|
+
if (targetMapName) {
|
|
171428
|
+
const vacName = entityId.replace("vacuum.", "");
|
|
171429
|
+
const selectedMapEntity = `select.${vacName}_selected_map`;
|
|
171430
|
+
logger236.info(
|
|
171431
|
+
`Dreame multi-floor: switching to map "${targetMapName}" via ${selectedMapEntity}`
|
|
171432
|
+
);
|
|
171433
|
+
seam.callAction({
|
|
171434
|
+
action: "select.select_option",
|
|
171435
|
+
target: selectedMapEntity,
|
|
171436
|
+
data: { option: targetMapName }
|
|
171437
|
+
});
|
|
171438
|
+
}
|
|
171439
|
+
return {
|
|
171440
|
+
action: {
|
|
171441
|
+
action: "dreame_vacuum.vacuum_clean_segment",
|
|
171442
|
+
data: { segments: roomIds.length === 1 ? roomIds[0] : roomIds }
|
|
171443
|
+
},
|
|
171444
|
+
pending: []
|
|
171445
|
+
};
|
|
171446
|
+
}
|
|
171447
|
+
if (isRoborockVacuum(attributes9) || isXiaomiMiotVacuum(attributes9)) {
|
|
171448
|
+
return {
|
|
171449
|
+
action: {
|
|
171450
|
+
action: "vacuum.send_command",
|
|
171451
|
+
data: { command: "app_segment_clean", params: roomIds }
|
|
171452
|
+
},
|
|
171453
|
+
pending: []
|
|
171454
|
+
};
|
|
171455
|
+
}
|
|
171456
|
+
if (isEcovacsVacuum(attributes9)) {
|
|
171457
|
+
const roomIdStr = roomIds.join(",");
|
|
171458
|
+
logger236.info(`Ecovacs vacuum: Using spot_area for rooms: ${roomIdStr}`);
|
|
171459
|
+
return {
|
|
171460
|
+
action: {
|
|
171461
|
+
action: "vacuum.send_command",
|
|
171462
|
+
data: {
|
|
171463
|
+
command: "spot_area",
|
|
171464
|
+
params: { mapID: 0, cleanings: 1, rooms: roomIdStr }
|
|
171465
|
+
}
|
|
171466
|
+
},
|
|
171467
|
+
pending: []
|
|
171468
|
+
};
|
|
171469
|
+
}
|
|
171470
|
+
logger236.warn(
|
|
171471
|
+
`Room cleaning via send_command not supported for this vacuum type. Rooms: ${roomIds.join(", ")}. Falling back to vacuum.start`
|
|
171472
|
+
);
|
|
171473
|
+
}
|
|
171474
|
+
return { action: { action: "vacuum.start" }, pending: [] };
|
|
171475
|
+
}
|
|
171316
171476
|
var vacuumRvcRunModeConfig = {
|
|
171317
171477
|
getCurrentMode: (entity) => {
|
|
171318
171478
|
const isCleaning = vacuumIsCleaning(entity.state);
|
|
@@ -171339,123 +171499,19 @@ var vacuumRvcRunModeConfig = {
|
|
|
171339
171499
|
const selectedAreas = [...serviceArea.state.selectedAreas];
|
|
171340
171500
|
if (selectedAreas.length > 0) {
|
|
171341
171501
|
const homeAssistant = agent.get(HomeAssistantEntityBehavior);
|
|
171342
|
-
const
|
|
171343
|
-
const attributes9 = entity.state.attributes;
|
|
171502
|
+
const effective = homeAssistant.endpoint.vacuumEffective;
|
|
171503
|
+
const attributes9 = (effective?.state ?? homeAssistant.entity.state).attributes;
|
|
171504
|
+
const mapping = effective ? effective.mapping : homeAssistant.state.mapping;
|
|
171344
171505
|
const session = getSession(homeAssistant.endpoint);
|
|
171345
|
-
const
|
|
171346
|
-
|
|
171347
|
-
|
|
171348
|
-
|
|
171349
|
-
|
|
171350
|
-
|
|
171351
|
-
|
|
171352
|
-
|
|
171353
|
-
|
|
171354
|
-
`CLEAN_AREA: cleaning HA areas: ${haAreaIds.join(", ")}`
|
|
171355
|
-
);
|
|
171356
|
-
return {
|
|
171357
|
-
action: "vacuum.clean_area",
|
|
171358
|
-
data: { cleaning_area_id: haAreaIds }
|
|
171359
|
-
};
|
|
171360
|
-
}
|
|
171361
|
-
}
|
|
171362
|
-
const roomEntities = homeAssistant.state.mapping?.roomEntities;
|
|
171363
|
-
if (roomEntities && roomEntities.length > 0) {
|
|
171364
|
-
const matched = [];
|
|
171365
|
-
for (const areaId of selectedAreas) {
|
|
171366
|
-
const entityId = roomEntities.find((id) => toAreaId(id) === areaId);
|
|
171367
|
-
if (entityId) {
|
|
171368
|
-
matched.push({ areaId, entityId });
|
|
171369
|
-
}
|
|
171370
|
-
}
|
|
171371
|
-
if (matched.length > 0) {
|
|
171372
|
-
logger236.info(
|
|
171373
|
-
`Roborock: ${matched.length} room button(s) queued: ${matched.map((m) => m.entityId).join(", ")}`
|
|
171374
|
-
);
|
|
171375
|
-
session.pendingDispatches = matched.slice(1).map(({ areaId, entityId }) => ({
|
|
171376
|
-
areaId,
|
|
171377
|
-
action: { action: "button.press", target: entityId }
|
|
171378
|
-
}));
|
|
171379
|
-
return {
|
|
171380
|
-
action: "button.press",
|
|
171381
|
-
target: matched[0].entityId
|
|
171382
|
-
};
|
|
171383
|
-
}
|
|
171384
|
-
}
|
|
171385
|
-
const vacuumEntityId = homeAssistant.entityId;
|
|
171386
|
-
if (vacuumEntityId.startsWith("vacuum.valetudo_")) {
|
|
171387
|
-
return buildValetudoSegmentAction(
|
|
171388
|
-
vacuumEntityId,
|
|
171389
|
-
selectedAreas,
|
|
171390
|
-
homeAssistant.state.mapping?.valetudoIdentifier
|
|
171391
|
-
);
|
|
171392
|
-
}
|
|
171393
|
-
const rooms = parseVacuumRooms(attributes9);
|
|
171394
|
-
const roomIds = [];
|
|
171395
|
-
let targetMapName;
|
|
171396
|
-
for (const areaId of selectedAreas) {
|
|
171397
|
-
const room = rooms.find((r) => toAreaId(r.id) === areaId);
|
|
171398
|
-
if (room) {
|
|
171399
|
-
roomIds.push(room.originalId ?? room.id);
|
|
171400
|
-
if (room.mapName && !targetMapName) {
|
|
171401
|
-
targetMapName = room.mapName;
|
|
171402
|
-
}
|
|
171403
|
-
}
|
|
171404
|
-
}
|
|
171405
|
-
if (roomIds.length > 0) {
|
|
171406
|
-
logger236.info(
|
|
171407
|
-
`Starting cleaning with selected areas: ${roomIds.join(", ")}`
|
|
171408
|
-
);
|
|
171409
|
-
if (isDreameVacuum(attributes9)) {
|
|
171410
|
-
if (targetMapName) {
|
|
171411
|
-
const vacName = vacuumEntityId.replace("vacuum.", "");
|
|
171412
|
-
const selectedMapEntity = `select.${vacName}_selected_map`;
|
|
171413
|
-
logger236.info(
|
|
171414
|
-
`Dreame multi-floor: switching to map "${targetMapName}" via ${selectedMapEntity}`
|
|
171415
|
-
);
|
|
171416
|
-
homeAssistant.callAction({
|
|
171417
|
-
action: "select.select_option",
|
|
171418
|
-
target: selectedMapEntity,
|
|
171419
|
-
data: { option: targetMapName }
|
|
171420
|
-
});
|
|
171421
|
-
}
|
|
171422
|
-
return {
|
|
171423
|
-
action: "dreame_vacuum.vacuum_clean_segment",
|
|
171424
|
-
data: {
|
|
171425
|
-
segments: roomIds.length === 1 ? roomIds[0] : roomIds
|
|
171426
|
-
}
|
|
171427
|
-
};
|
|
171428
|
-
}
|
|
171429
|
-
if (isRoborockVacuum(attributes9) || isXiaomiMiotVacuum(attributes9)) {
|
|
171430
|
-
return {
|
|
171431
|
-
action: "vacuum.send_command",
|
|
171432
|
-
data: {
|
|
171433
|
-
command: "app_segment_clean",
|
|
171434
|
-
params: roomIds
|
|
171435
|
-
}
|
|
171436
|
-
};
|
|
171437
|
-
}
|
|
171438
|
-
if (isEcovacsVacuum(attributes9)) {
|
|
171439
|
-
const roomIdStr = roomIds.join(",");
|
|
171440
|
-
logger236.info(
|
|
171441
|
-
`Ecovacs vacuum: Using spot_area for rooms: ${roomIdStr}`
|
|
171442
|
-
);
|
|
171443
|
-
return {
|
|
171444
|
-
action: "vacuum.send_command",
|
|
171445
|
-
data: {
|
|
171446
|
-
command: "spot_area",
|
|
171447
|
-
params: {
|
|
171448
|
-
mapID: 0,
|
|
171449
|
-
cleanings: 1,
|
|
171450
|
-
rooms: roomIdStr
|
|
171451
|
-
}
|
|
171452
|
-
}
|
|
171453
|
-
};
|
|
171454
|
-
}
|
|
171455
|
-
logger236.warn(
|
|
171456
|
-
`Room cleaning via send_command not supported for this vacuum type. Rooms: ${roomIds.join(", ")}. Falling back to vacuum.start`
|
|
171457
|
-
);
|
|
171458
|
-
}
|
|
171506
|
+
const result = dispatchRoomClean(
|
|
171507
|
+
attributes9,
|
|
171508
|
+
mapping,
|
|
171509
|
+
homeAssistant.entityId,
|
|
171510
|
+
selectedAreas,
|
|
171511
|
+
{ callAction: (a) => homeAssistant.callAction(a) }
|
|
171512
|
+
);
|
|
171513
|
+
session.pendingDispatches = result.pending;
|
|
171514
|
+
return result.action;
|
|
171459
171515
|
}
|
|
171460
171516
|
} catch {
|
|
171461
171517
|
}
|
|
@@ -173922,6 +173978,12 @@ function asStandaloneEndpointType(type) {
|
|
|
173922
173978
|
// src/matter/endpoints/legacy/legacy-endpoint.ts
|
|
173923
173979
|
var logger245 = Logger.get("LegacyEndpoint");
|
|
173924
173980
|
var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
|
|
173981
|
+
constructor(type, entityId, customName, mappedEntityIds, throttleMs, endpointId, vacuumEffective) {
|
|
173982
|
+
super(type, entityId, customName, mappedEntityIds, endpointId);
|
|
173983
|
+
this.vacuumEffective = vacuumEffective;
|
|
173984
|
+
this.flushUpdate = throttleMs && throttleMs > 50 ? throttleLatest(this.flushPendingUpdate.bind(this), throttleMs) : debounce6(this.flushPendingUpdate.bind(this), 50);
|
|
173985
|
+
}
|
|
173986
|
+
vacuumEffective;
|
|
173925
173987
|
static async create(registry3, entityId, mapping, pluginDomainMappings, standalone = false, endpointId, identityAnchor) {
|
|
173926
173988
|
const deviceRegistry = registry3.deviceOf(entityId);
|
|
173927
173989
|
let state = registry3.initialState(entityId);
|
|
@@ -174295,19 +174357,17 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
|
|
|
174295
174357
|
}
|
|
174296
174358
|
const customName = effectiveMapping?.customName;
|
|
174297
174359
|
const mappedIds = getMappedEntityIds(effectiveMapping);
|
|
174360
|
+
const vacuumEffective = entityId.startsWith("vacuum.") ? { mapping: effectiveMapping, state } : void 0;
|
|
174298
174361
|
return new _LegacyEndpoint(
|
|
174299
174362
|
type,
|
|
174300
174363
|
entityId,
|
|
174301
174364
|
customName,
|
|
174302
174365
|
mappedIds,
|
|
174303
174366
|
effectiveMapping?.updateThrottleMs,
|
|
174304
|
-
endpointId
|
|
174367
|
+
endpointId,
|
|
174368
|
+
vacuumEffective
|
|
174305
174369
|
);
|
|
174306
174370
|
}
|
|
174307
|
-
constructor(type, entityId, customName, mappedEntityIds, throttleMs, endpointId) {
|
|
174308
|
-
super(type, entityId, customName, mappedEntityIds, endpointId);
|
|
174309
|
-
this.flushUpdate = throttleMs && throttleMs > 50 ? throttleLatest(this.flushPendingUpdate.bind(this), throttleMs) : debounce6(this.flushPendingUpdate.bind(this), 50);
|
|
174310
|
-
}
|
|
174311
174371
|
lastState;
|
|
174312
174372
|
pendingMappedChange = false;
|
|
174313
174373
|
flushUpdate;
|
|
@@ -174374,6 +174434,96 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
|
|
|
174374
174434
|
}
|
|
174375
174435
|
};
|
|
174376
174436
|
|
|
174437
|
+
// src/matter/endpoints/legacy/vacuum/vacuum-area-switch.ts
|
|
174438
|
+
init_esm();
|
|
174439
|
+
init_home_assistant_entity_behavior();
|
|
174440
|
+
var VacuumAreaSwitchEndpoint = class _VacuumAreaSwitchEndpoint extends EntityEndpoint {
|
|
174441
|
+
constructor(type, entityId, endpointId, areaId, vacuumEndpointId, parentEffective) {
|
|
174442
|
+
super(type, entityId, void 0, [], endpointId);
|
|
174443
|
+
this.areaId = areaId;
|
|
174444
|
+
this.vacuumEndpointId = vacuumEndpointId;
|
|
174445
|
+
this.parentEffective = parentEffective;
|
|
174446
|
+
}
|
|
174447
|
+
areaId;
|
|
174448
|
+
vacuumEndpointId;
|
|
174449
|
+
parentEffective;
|
|
174450
|
+
static create(params) {
|
|
174451
|
+
const { vacuumEndpointId, entity, mapping, area, parentEffective } = params;
|
|
174452
|
+
const endpointId = `${vacuumEndpointId}_roomsw_${area.areaId}`;
|
|
174453
|
+
const areaId = area.areaId;
|
|
174454
|
+
const switchMapping = mapping?.customSerialNumber ? { ...mapping, customSerialNumber: void 0 } : mapping;
|
|
174455
|
+
const switchEntity = entity.deviceRegistry?.serial_number ? {
|
|
174456
|
+
...entity,
|
|
174457
|
+
deviceRegistry: {
|
|
174458
|
+
...entity.deviceRegistry,
|
|
174459
|
+
serial_number: void 0
|
|
174460
|
+
}
|
|
174461
|
+
} : entity;
|
|
174462
|
+
const type = OnOffPlugInUnitDevice.with(
|
|
174463
|
+
BasicInformationServer2,
|
|
174464
|
+
IdentifyServer2,
|
|
174465
|
+
HomeAssistantEntityBehavior,
|
|
174466
|
+
OnOffServer2({
|
|
174467
|
+
isOn: () => false,
|
|
174468
|
+
// off is a no-op (momentary auto-reset only, script pattern).
|
|
174469
|
+
turnOff: null,
|
|
174470
|
+
turnOn: (_value, agent) => {
|
|
174471
|
+
const ha = agent.get(HomeAssistantEntityBehavior);
|
|
174472
|
+
const { action } = dispatchRoomClean(
|
|
174473
|
+
parentEffective.state.attributes,
|
|
174474
|
+
parentEffective.mapping,
|
|
174475
|
+
ha.entityId,
|
|
174476
|
+
[areaId],
|
|
174477
|
+
{ callAction: (a) => ha.callAction(a) }
|
|
174478
|
+
);
|
|
174479
|
+
return action;
|
|
174480
|
+
}
|
|
174481
|
+
})
|
|
174482
|
+
).set({
|
|
174483
|
+
homeAssistantEntity: {
|
|
174484
|
+
entity: switchEntity,
|
|
174485
|
+
mapping: switchMapping,
|
|
174486
|
+
customName: area.name,
|
|
174487
|
+
// Distinct anchor so uniqueId/serial don't collide with the vacuum's,
|
|
174488
|
+
// and stay frozen across HA renames.
|
|
174489
|
+
identityAnchor: endpointId
|
|
174490
|
+
}
|
|
174491
|
+
});
|
|
174492
|
+
return new _VacuumAreaSwitchEndpoint(
|
|
174493
|
+
type,
|
|
174494
|
+
entity.entity_id,
|
|
174495
|
+
endpointId,
|
|
174496
|
+
areaId,
|
|
174497
|
+
vacuumEndpointId,
|
|
174498
|
+
parentEffective
|
|
174499
|
+
);
|
|
174500
|
+
}
|
|
174501
|
+
async updateStates(states) {
|
|
174502
|
+
const state = states[this.entityId];
|
|
174503
|
+
if (!state) return;
|
|
174504
|
+
try {
|
|
174505
|
+
await this.construction.ready;
|
|
174506
|
+
} catch {
|
|
174507
|
+
return;
|
|
174508
|
+
}
|
|
174509
|
+
try {
|
|
174510
|
+
const current = this.stateOf(HomeAssistantEntityBehavior).entity;
|
|
174511
|
+
await this.setStateOf(HomeAssistantEntityBehavior, {
|
|
174512
|
+
entity: { ...current, state }
|
|
174513
|
+
});
|
|
174514
|
+
} catch (error) {
|
|
174515
|
+
if (error instanceof TransactionDestroyedError || error instanceof DestroyedDependencyError) {
|
|
174516
|
+
return;
|
|
174517
|
+
}
|
|
174518
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
174519
|
+
if (message.includes("Endpoint storage inaccessible")) {
|
|
174520
|
+
return;
|
|
174521
|
+
}
|
|
174522
|
+
throw error;
|
|
174523
|
+
}
|
|
174524
|
+
}
|
|
174525
|
+
};
|
|
174526
|
+
|
|
174377
174527
|
// src/plugins/builtin/camera/camera-plugin.ts
|
|
174378
174528
|
init_esm();
|
|
174379
174529
|
|
|
@@ -175365,9 +175515,943 @@ var CameraPlugin = class {
|
|
|
175365
175515
|
}
|
|
175366
175516
|
};
|
|
175367
175517
|
|
|
175518
|
+
// src/plugins/builtin/security/security-plugin.ts
|
|
175519
|
+
init_esm();
|
|
175520
|
+
import {
|
|
175521
|
+
callService as callService2,
|
|
175522
|
+
createConnection as createConnection4,
|
|
175523
|
+
createLongLivedTokenAuth as createLongLivedTokenAuth3
|
|
175524
|
+
} from "home-assistant-js-websocket";
|
|
175525
|
+
|
|
175526
|
+
// src/plugins/builtin/security/security-state-machine.ts
|
|
175527
|
+
var ARM_MODES = [
|
|
175528
|
+
"home",
|
|
175529
|
+
"away",
|
|
175530
|
+
"night",
|
|
175531
|
+
"vacation"
|
|
175532
|
+
];
|
|
175533
|
+
var defaultSecurityScheduler = {
|
|
175534
|
+
schedule(ms, fn) {
|
|
175535
|
+
const timer = setTimeout(fn, ms);
|
|
175536
|
+
timer.unref?.();
|
|
175537
|
+
return () => clearTimeout(timer);
|
|
175538
|
+
}
|
|
175539
|
+
};
|
|
175540
|
+
function parseEntityList(value) {
|
|
175541
|
+
if (typeof value !== "string") return [];
|
|
175542
|
+
return [
|
|
175543
|
+
...new Set(
|
|
175544
|
+
value.split(",").map((s) => s.trim()).filter(Boolean)
|
|
175545
|
+
)
|
|
175546
|
+
];
|
|
175547
|
+
}
|
|
175548
|
+
var SILENCEABLE_ALERT_DOMAINS = /* @__PURE__ */ new Set([
|
|
175549
|
+
"siren",
|
|
175550
|
+
"switch",
|
|
175551
|
+
"light"
|
|
175552
|
+
]);
|
|
175553
|
+
var FIRE_ONLY_ALERT_DOMAINS = /* @__PURE__ */ new Set([
|
|
175554
|
+
"script",
|
|
175555
|
+
"scene"
|
|
175556
|
+
]);
|
|
175557
|
+
function resolveSecurityLists(config8, warn) {
|
|
175558
|
+
const warned = /* @__PURE__ */ new Set();
|
|
175559
|
+
const alerts = (value) => parseEntityList(value).filter((id) => {
|
|
175560
|
+
const domain = id.split(".")[0];
|
|
175561
|
+
if (SILENCEABLE_ALERT_DOMAINS.has(domain) || FIRE_ONLY_ALERT_DOMAINS.has(domain)) {
|
|
175562
|
+
return true;
|
|
175563
|
+
}
|
|
175564
|
+
if (!warned.has(id)) {
|
|
175565
|
+
warned.add(id);
|
|
175566
|
+
warn?.(
|
|
175567
|
+
`alert entity ${id} dropped, only siren/switch/light and script/scene are supported`
|
|
175568
|
+
);
|
|
175569
|
+
}
|
|
175570
|
+
return false;
|
|
175571
|
+
});
|
|
175572
|
+
const awaySetters = parseEntityList(config8.awaySetters);
|
|
175573
|
+
const awayTriggers = parseEntityList(config8.awayTriggers);
|
|
175574
|
+
const awayAlerts = alerts(config8.awayAlerts);
|
|
175575
|
+
const vacationSetters = parseEntityList(config8.vacationSetters);
|
|
175576
|
+
const vacationTriggers = parseEntityList(config8.vacationTriggers);
|
|
175577
|
+
const vacationAlerts = alerts(config8.vacationAlerts);
|
|
175578
|
+
return {
|
|
175579
|
+
setters: {
|
|
175580
|
+
home: parseEntityList(config8.homeSetters),
|
|
175581
|
+
away: awaySetters,
|
|
175582
|
+
night: parseEntityList(config8.nightSetters),
|
|
175583
|
+
vacation: vacationSetters.length > 0 ? vacationSetters : awaySetters
|
|
175584
|
+
},
|
|
175585
|
+
offSetters: parseEntityList(config8.offSetters),
|
|
175586
|
+
triggers: {
|
|
175587
|
+
home: parseEntityList(config8.homeTriggers),
|
|
175588
|
+
away: awayTriggers,
|
|
175589
|
+
night: parseEntityList(config8.nightTriggers),
|
|
175590
|
+
vacation: vacationTriggers.length > 0 ? vacationTriggers : awayTriggers
|
|
175591
|
+
},
|
|
175592
|
+
triggers24h: parseEntityList(config8.triggers24h),
|
|
175593
|
+
alerts: {
|
|
175594
|
+
home: alerts(config8.homeAlerts),
|
|
175595
|
+
away: awayAlerts,
|
|
175596
|
+
night: alerts(config8.nightAlerts),
|
|
175597
|
+
vacation: vacationAlerts.length > 0 ? vacationAlerts : awayAlerts
|
|
175598
|
+
},
|
|
175599
|
+
alerts24h: alerts(config8.alerts24h),
|
|
175600
|
+
alwaysAlerts: alerts(config8.alwaysAlerts)
|
|
175601
|
+
};
|
|
175602
|
+
}
|
|
175603
|
+
function alertsForTier(lists, tier) {
|
|
175604
|
+
const base = tier === "24h" ? lists.alerts24h : lists.alerts[tier];
|
|
175605
|
+
return [.../* @__PURE__ */ new Set([...base, ...lists.alwaysAlerts])];
|
|
175606
|
+
}
|
|
175607
|
+
function watchedTriggerEntities(lists) {
|
|
175608
|
+
const all = new Set(lists.triggers24h);
|
|
175609
|
+
for (const mode of ARM_MODES) {
|
|
175610
|
+
for (const id of lists.triggers[mode]) all.add(id);
|
|
175611
|
+
}
|
|
175612
|
+
return all;
|
|
175613
|
+
}
|
|
175614
|
+
var PERIMETER_DEVICE_CLASSES = /* @__PURE__ */ new Set([
|
|
175615
|
+
"door",
|
|
175616
|
+
"window",
|
|
175617
|
+
"garage_door",
|
|
175618
|
+
"opening"
|
|
175619
|
+
]);
|
|
175620
|
+
function isPerimeterTrigger(entityId, deviceClass) {
|
|
175621
|
+
return entityId.startsWith("binary_sensor.") && deviceClass != null && PERIMETER_DEVICE_CLASSES.has(deviceClass);
|
|
175622
|
+
}
|
|
175623
|
+
var SecurityStateMachine = class {
|
|
175624
|
+
constructor(config8, effects, scheduler = defaultSecurityScheduler) {
|
|
175625
|
+
this.config = config8;
|
|
175626
|
+
this.effects = effects;
|
|
175627
|
+
this.scheduler = scheduler;
|
|
175628
|
+
}
|
|
175629
|
+
config;
|
|
175630
|
+
effects;
|
|
175631
|
+
scheduler;
|
|
175632
|
+
mode = null;
|
|
175633
|
+
phase = "disarmed";
|
|
175634
|
+
// Where a triggered alarm returns to when the trigger time runs out.
|
|
175635
|
+
returnMode = null;
|
|
175636
|
+
// Tier of the current trip, only meaningful while triggered.
|
|
175637
|
+
trippedTier = null;
|
|
175638
|
+
// False while an exit delay is still running, so an auto-return after a
|
|
175639
|
+
// trip that cut the delay short can run the setters late.
|
|
175640
|
+
modeReachedRan = true;
|
|
175641
|
+
cancelTimer;
|
|
175642
|
+
get snapshot() {
|
|
175643
|
+
return { mode: this.mode, phase: this.phase };
|
|
175644
|
+
}
|
|
175645
|
+
// Live config swap: state survives, already running timers keep the delay
|
|
175646
|
+
// they started with.
|
|
175647
|
+
setConfig(config8) {
|
|
175648
|
+
this.config = config8;
|
|
175649
|
+
}
|
|
175650
|
+
// Applied once on start from the persisted snapshot. Interrupted delays
|
|
175651
|
+
// resolve to armed; triggered stays only when the trigger time is infinite.
|
|
175652
|
+
// A resolution that changed the phase is persisted right away so a second
|
|
175653
|
+
// restart replays no effects. An interrupted exit delay is the one case
|
|
175654
|
+
// where the setters never ran, so that restore dispatches modeReached.
|
|
175655
|
+
restore(snapshot) {
|
|
175656
|
+
if (!snapshot) return;
|
|
175657
|
+
const mode = ARM_MODES.includes(snapshot.mode) ? snapshot.mode : null;
|
|
175658
|
+
const reached = snapshot.modeReached ?? true;
|
|
175659
|
+
switch (snapshot.phase) {
|
|
175660
|
+
case "arming":
|
|
175661
|
+
case "pending":
|
|
175662
|
+
case "armed":
|
|
175663
|
+
if (mode) {
|
|
175664
|
+
this.mode = mode;
|
|
175665
|
+
this.phase = "armed";
|
|
175666
|
+
this.modeReachedRan = true;
|
|
175667
|
+
if (snapshot.phase !== "armed") this.persist();
|
|
175668
|
+
if (snapshot.phase === "arming") this.effects.modeReached(mode);
|
|
175669
|
+
}
|
|
175670
|
+
break;
|
|
175671
|
+
case "triggered":
|
|
175672
|
+
if (this.config.triggerTimeSeconds === 0) {
|
|
175673
|
+
this.mode = mode;
|
|
175674
|
+
this.returnMode = mode;
|
|
175675
|
+
this.phase = "triggered";
|
|
175676
|
+
this.modeReachedRan = reached;
|
|
175677
|
+
} else {
|
|
175678
|
+
if (mode) {
|
|
175679
|
+
this.mode = mode;
|
|
175680
|
+
this.phase = "armed";
|
|
175681
|
+
this.modeReachedRan = true;
|
|
175682
|
+
}
|
|
175683
|
+
this.persist();
|
|
175684
|
+
if (mode && !reached) this.effects.modeReached(mode);
|
|
175685
|
+
}
|
|
175686
|
+
break;
|
|
175687
|
+
default:
|
|
175688
|
+
break;
|
|
175689
|
+
}
|
|
175690
|
+
}
|
|
175691
|
+
// A mode switch changed on the Matter side. Exclusivity lives here: arming
|
|
175692
|
+
// one mode reports the other three off, turning the active one off disarms.
|
|
175693
|
+
handleModeSwitch(mode, on) {
|
|
175694
|
+
if (on) {
|
|
175695
|
+
if (this.mode === mode && this.phase !== "disarmed") {
|
|
175696
|
+
this.reportSwitches();
|
|
175697
|
+
return;
|
|
175698
|
+
}
|
|
175699
|
+
this.arm(mode);
|
|
175700
|
+
} else if (this.mode === mode && this.phase !== "disarmed" || this.mode === null && this.phase === "triggered") {
|
|
175701
|
+
this.disarm();
|
|
175702
|
+
} else {
|
|
175703
|
+
this.reportSwitches();
|
|
175704
|
+
}
|
|
175705
|
+
}
|
|
175706
|
+
arm(mode) {
|
|
175707
|
+
const wasTriggered = this.phase === "triggered";
|
|
175708
|
+
this.clearTimer();
|
|
175709
|
+
this.mode = mode;
|
|
175710
|
+
this.returnMode = null;
|
|
175711
|
+
const delayMs = this.config.exitDelaySeconds * 1e3;
|
|
175712
|
+
if (delayMs > 0) {
|
|
175713
|
+
this.phase = "arming";
|
|
175714
|
+
this.modeReachedRan = false;
|
|
175715
|
+
this.reportSwitches();
|
|
175716
|
+
if (wasTriggered) this.effects.alarmCleared();
|
|
175717
|
+
this.persist();
|
|
175718
|
+
this.cancelTimer = this.scheduler.schedule(
|
|
175719
|
+
delayMs,
|
|
175720
|
+
() => this.becomeArmed()
|
|
175721
|
+
);
|
|
175722
|
+
} else {
|
|
175723
|
+
this.phase = "armed";
|
|
175724
|
+
this.modeReachedRan = true;
|
|
175725
|
+
this.reportSwitches();
|
|
175726
|
+
if (wasTriggered) this.effects.alarmCleared();
|
|
175727
|
+
this.persist();
|
|
175728
|
+
this.effects.modeReached(mode);
|
|
175729
|
+
}
|
|
175730
|
+
}
|
|
175731
|
+
disarm() {
|
|
175732
|
+
if (this.phase === "disarmed") {
|
|
175733
|
+
this.reportSwitches();
|
|
175734
|
+
return;
|
|
175735
|
+
}
|
|
175736
|
+
const wasTriggered = this.phase === "triggered";
|
|
175737
|
+
this.clearTimer();
|
|
175738
|
+
this.mode = null;
|
|
175739
|
+
this.returnMode = null;
|
|
175740
|
+
this.phase = "disarmed";
|
|
175741
|
+
this.reportSwitches();
|
|
175742
|
+
if (wasTriggered) this.effects.alarmCleared();
|
|
175743
|
+
this.persist();
|
|
175744
|
+
this.effects.disarmed();
|
|
175745
|
+
}
|
|
175746
|
+
// A watched entity entered on/open. The 24h list trips in every state and
|
|
175747
|
+
// skips the entry delay; mode triggers only count while armed (the exit
|
|
175748
|
+
// delay exists so leaving does not trip the alarm).
|
|
175749
|
+
handleEntityOn(entityId, isPerimeter) {
|
|
175750
|
+
if (this.config.triggers24h.includes(entityId)) {
|
|
175751
|
+
if (this.phase !== "triggered" || this.trippedTier !== "24h") {
|
|
175752
|
+
this.trip("24h", entityId);
|
|
175753
|
+
}
|
|
175754
|
+
return;
|
|
175755
|
+
}
|
|
175756
|
+
const mode = this.mode;
|
|
175757
|
+
if (!mode) return;
|
|
175758
|
+
if (!this.config.triggers[mode].includes(entityId)) return;
|
|
175759
|
+
if (this.phase === "armed") {
|
|
175760
|
+
const entryMs = this.config.entryDelaySeconds * 1e3;
|
|
175761
|
+
if (isPerimeter && entryMs > 0) {
|
|
175762
|
+
this.phase = "pending";
|
|
175763
|
+
this.persist();
|
|
175764
|
+
this.cancelTimer = this.scheduler.schedule(
|
|
175765
|
+
entryMs,
|
|
175766
|
+
() => this.trip(mode, entityId)
|
|
175767
|
+
);
|
|
175768
|
+
} else {
|
|
175769
|
+
this.trip(mode, entityId);
|
|
175770
|
+
}
|
|
175771
|
+
} else if (this.phase === "pending" && !isPerimeter) {
|
|
175772
|
+
this.trip(mode, entityId);
|
|
175773
|
+
}
|
|
175774
|
+
}
|
|
175775
|
+
shutdown() {
|
|
175776
|
+
this.clearTimer();
|
|
175777
|
+
}
|
|
175778
|
+
becomeArmed() {
|
|
175779
|
+
this.cancelTimer = void 0;
|
|
175780
|
+
const mode = this.mode;
|
|
175781
|
+
if (!mode || this.phase !== "arming") return;
|
|
175782
|
+
this.phase = "armed";
|
|
175783
|
+
this.modeReachedRan = true;
|
|
175784
|
+
this.persist();
|
|
175785
|
+
this.effects.modeReached(mode);
|
|
175786
|
+
}
|
|
175787
|
+
trip(tier, entityId) {
|
|
175788
|
+
this.clearTimer();
|
|
175789
|
+
this.returnMode = this.mode;
|
|
175790
|
+
this.trippedTier = tier;
|
|
175791
|
+
this.phase = "triggered";
|
|
175792
|
+
this.effects.tripped(tier, entityId);
|
|
175793
|
+
this.persist();
|
|
175794
|
+
const holdMs = this.config.triggerTimeSeconds * 1e3;
|
|
175795
|
+
if (holdMs > 0) {
|
|
175796
|
+
this.cancelTimer = this.scheduler.schedule(
|
|
175797
|
+
holdMs,
|
|
175798
|
+
() => this.autoReturn()
|
|
175799
|
+
);
|
|
175800
|
+
}
|
|
175801
|
+
}
|
|
175802
|
+
autoReturn() {
|
|
175803
|
+
this.cancelTimer = void 0;
|
|
175804
|
+
this.mode = this.returnMode;
|
|
175805
|
+
this.returnMode = null;
|
|
175806
|
+
this.phase = this.mode ? "armed" : "disarmed";
|
|
175807
|
+
const lateSetters = this.mode != null && !this.modeReachedRan;
|
|
175808
|
+
this.modeReachedRan = true;
|
|
175809
|
+
this.effects.alarmCleared();
|
|
175810
|
+
this.persist();
|
|
175811
|
+
if (lateSetters && this.mode) this.effects.modeReached(this.mode);
|
|
175812
|
+
}
|
|
175813
|
+
reportSwitches() {
|
|
175814
|
+
const active = this.phase === "disarmed" ? null : this.mode;
|
|
175815
|
+
const states = {};
|
|
175816
|
+
for (const mode of ARM_MODES) states[mode] = mode === active;
|
|
175817
|
+
this.effects.switchStates(states);
|
|
175818
|
+
}
|
|
175819
|
+
persist() {
|
|
175820
|
+
this.effects.persist({
|
|
175821
|
+
...this.snapshot,
|
|
175822
|
+
modeReached: this.modeReachedRan
|
|
175823
|
+
});
|
|
175824
|
+
}
|
|
175825
|
+
clearTimer() {
|
|
175826
|
+
this.cancelTimer?.();
|
|
175827
|
+
this.cancelTimer = void 0;
|
|
175828
|
+
}
|
|
175829
|
+
};
|
|
175830
|
+
|
|
175831
|
+
// src/plugins/builtin/security/security-plugin.ts
|
|
175832
|
+
var CONFIG_KEY2 = "config";
|
|
175833
|
+
var STATE_KEY = "state";
|
|
175834
|
+
var PENDING_SILENCE_KEY = "pendingSilence";
|
|
175835
|
+
var PENDING_SETTERS_KEY = "pendingSetters";
|
|
175836
|
+
var ALARM_DEVICE_ID = "alarm";
|
|
175837
|
+
var MODE_DEVICE_IDS = {
|
|
175838
|
+
home: "mode_home",
|
|
175839
|
+
away: "mode_away",
|
|
175840
|
+
night: "mode_night",
|
|
175841
|
+
vacation: "mode_vacation"
|
|
175842
|
+
};
|
|
175843
|
+
var MODE_NAMES = {
|
|
175844
|
+
home: "Home",
|
|
175845
|
+
away: "Away",
|
|
175846
|
+
night: "Night",
|
|
175847
|
+
vacation: "Vacation"
|
|
175848
|
+
};
|
|
175849
|
+
var RETRY_BASE_MS = 1e3;
|
|
175850
|
+
var RETRY_MAX_MS = 6e4;
|
|
175851
|
+
var CALL_DEADLINE_MS = 1e4;
|
|
175852
|
+
var SecurityPlugin = class {
|
|
175853
|
+
constructor(config8 = {}, deps = {}) {
|
|
175854
|
+
this.deps = deps;
|
|
175855
|
+
this.config = config8;
|
|
175856
|
+
}
|
|
175857
|
+
deps;
|
|
175858
|
+
name = "security";
|
|
175859
|
+
version = "0.1.0";
|
|
175860
|
+
log = Logger.get("SecurityPlugin");
|
|
175861
|
+
context;
|
|
175862
|
+
config;
|
|
175863
|
+
lists = resolveSecurityLists({});
|
|
175864
|
+
watched = /* @__PURE__ */ new Set();
|
|
175865
|
+
machine;
|
|
175866
|
+
activeAlerts = [];
|
|
175867
|
+
// Effect tasks run strictly FIFO: a disarm's silence can never overtake the
|
|
175868
|
+
// trip's turn_on loop it is chasing. Queued (not yet started) intent is
|
|
175869
|
+
// coalesced per entity so a hung call cannot pile up work behind it.
|
|
175870
|
+
tasks = [];
|
|
175871
|
+
draining = false;
|
|
175872
|
+
connection;
|
|
175873
|
+
unsubscribeEvents;
|
|
175874
|
+
retryTimer;
|
|
175875
|
+
backoffMs = RETRY_BASE_MS;
|
|
175876
|
+
// Bumped on every teardown; a dial resolving under a stale generation is
|
|
175877
|
+
// discarded instead of installing an old target.
|
|
175878
|
+
dialGeneration = 0;
|
|
175879
|
+
async onStart(context) {
|
|
175880
|
+
this.context = context;
|
|
175881
|
+
const stored = await context.storage.get(CONFIG_KEY2);
|
|
175882
|
+
this.config = { ...this.config, ...stored ?? {} };
|
|
175883
|
+
this.applyLists();
|
|
175884
|
+
this.machine = new SecurityStateMachine(
|
|
175885
|
+
this.machineConfig(),
|
|
175886
|
+
this.effects()
|
|
175887
|
+
);
|
|
175888
|
+
await this.registerDevices();
|
|
175889
|
+
const state = await context.storage.get(STATE_KEY);
|
|
175890
|
+
this.machine.restore(state);
|
|
175891
|
+
await context.storage.flush?.();
|
|
175892
|
+
if (state?.phase === "triggered") {
|
|
175893
|
+
const known = state.activeAlerts;
|
|
175894
|
+
if (this.machine.snapshot.phase === "triggered") {
|
|
175895
|
+
this.activeAlerts = known ?? this.allSilenceableAlerts();
|
|
175896
|
+
} else {
|
|
175897
|
+
const candidates = (known ?? this.allSilenceableAlerts()).filter(
|
|
175898
|
+
(id) => SILENCEABLE_ALERT_DOMAINS.has(id.split(".")[0])
|
|
175899
|
+
);
|
|
175900
|
+
this.enqueueSilence(candidates);
|
|
175901
|
+
}
|
|
175902
|
+
}
|
|
175903
|
+
this.pushDeviceStates();
|
|
175904
|
+
this.startConnection();
|
|
175905
|
+
}
|
|
175906
|
+
async onConfigChanged(config8) {
|
|
175907
|
+
this.config = config8;
|
|
175908
|
+
await this.context?.storage.set(CONFIG_KEY2, this.config);
|
|
175909
|
+
this.applyLists();
|
|
175910
|
+
this.machine?.setConfig(this.machineConfig());
|
|
175911
|
+
await this.stopConnection();
|
|
175912
|
+
this.startConnection();
|
|
175913
|
+
}
|
|
175914
|
+
async onShutdown() {
|
|
175915
|
+
this.machine?.shutdown();
|
|
175916
|
+
await this.stopConnection();
|
|
175917
|
+
for (const id of [...Object.values(MODE_DEVICE_IDS), ALARM_DEVICE_ID]) {
|
|
175918
|
+
await this.context?.unregisterDevice(id).catch(() => {
|
|
175919
|
+
});
|
|
175920
|
+
}
|
|
175921
|
+
}
|
|
175922
|
+
getCurrentConfig() {
|
|
175923
|
+
return { ...this.config };
|
|
175924
|
+
}
|
|
175925
|
+
getConfigSchema() {
|
|
175926
|
+
const entityList = (title, description) => ({
|
|
175927
|
+
type: "string",
|
|
175928
|
+
title,
|
|
175929
|
+
description,
|
|
175930
|
+
required: false
|
|
175931
|
+
});
|
|
175932
|
+
return {
|
|
175933
|
+
title: "Security",
|
|
175934
|
+
description: "A small alarm system: four exclusive mode switches plus an Alarm contact sensor. All entity fields are comma-separated lists.",
|
|
175935
|
+
properties: {
|
|
175936
|
+
exitDelaySeconds: {
|
|
175937
|
+
type: "number",
|
|
175938
|
+
title: "Exit delay (seconds)",
|
|
175939
|
+
description: "Delay before an armed mode takes effect. 0 disables.",
|
|
175940
|
+
default: 60,
|
|
175941
|
+
required: false
|
|
175942
|
+
},
|
|
175943
|
+
entryDelaySeconds: {
|
|
175944
|
+
type: "number",
|
|
175945
|
+
title: "Entry delay (seconds)",
|
|
175946
|
+
description: "Delay for door/window/garage door/opening sensors while armed. Other trigger classes trip instantly. 0 disables.",
|
|
175947
|
+
default: 60,
|
|
175948
|
+
required: false
|
|
175949
|
+
},
|
|
175950
|
+
triggerTimeSeconds: {
|
|
175951
|
+
type: "number",
|
|
175952
|
+
title: "Trigger time (seconds)",
|
|
175953
|
+
description: "How long the alarm stays triggered before returning to the state it was tripped from. 0 keeps it triggered until disarm.",
|
|
175954
|
+
default: 120,
|
|
175955
|
+
required: false
|
|
175956
|
+
},
|
|
175957
|
+
homeSetters: entityList(
|
|
175958
|
+
"Home setters",
|
|
175959
|
+
"Invoked when Home is armed, e.g. scene.arm_home,script.notify"
|
|
175960
|
+
),
|
|
175961
|
+
homeTriggers: entityList(
|
|
175962
|
+
"Home triggers",
|
|
175963
|
+
"Sensors that trip the alarm while armed Home."
|
|
175964
|
+
),
|
|
175965
|
+
homeAlerts: entityList(
|
|
175966
|
+
"Home alerts",
|
|
175967
|
+
"Turned on when the alarm trips while armed Home."
|
|
175968
|
+
),
|
|
175969
|
+
awaySetters: entityList("Away setters", "Invoked when Away is armed."),
|
|
175970
|
+
awayTriggers: entityList(
|
|
175971
|
+
"Away triggers",
|
|
175972
|
+
"Sensors that trip the alarm while armed Away."
|
|
175973
|
+
),
|
|
175974
|
+
awayAlerts: entityList(
|
|
175975
|
+
"Away alerts",
|
|
175976
|
+
"Turned on when the alarm trips while armed Away."
|
|
175977
|
+
),
|
|
175978
|
+
nightSetters: entityList(
|
|
175979
|
+
"Night setters",
|
|
175980
|
+
"Invoked when Night is armed."
|
|
175981
|
+
),
|
|
175982
|
+
nightTriggers: entityList(
|
|
175983
|
+
"Night triggers",
|
|
175984
|
+
"Sensors that trip the alarm while armed Night."
|
|
175985
|
+
),
|
|
175986
|
+
nightAlerts: entityList(
|
|
175987
|
+
"Night alerts",
|
|
175988
|
+
"Turned on when the alarm trips while armed Night."
|
|
175989
|
+
),
|
|
175990
|
+
vacationSetters: entityList(
|
|
175991
|
+
"Vacation setters",
|
|
175992
|
+
"Leave empty to use the Away setters."
|
|
175993
|
+
),
|
|
175994
|
+
vacationTriggers: entityList(
|
|
175995
|
+
"Vacation triggers",
|
|
175996
|
+
"Leave empty to use the Away triggers."
|
|
175997
|
+
),
|
|
175998
|
+
vacationAlerts: entityList(
|
|
175999
|
+
"Vacation alerts",
|
|
176000
|
+
"Leave empty to use the Away alerts."
|
|
176001
|
+
),
|
|
176002
|
+
offSetters: entityList(
|
|
176003
|
+
"Off setters",
|
|
176004
|
+
"Invoked on disarm, e.g. scene.alarm_off"
|
|
176005
|
+
),
|
|
176006
|
+
triggers24h: entityList(
|
|
176007
|
+
"24h triggers",
|
|
176008
|
+
"Trip the alarm in every state including disarmed, without entry delay. For smoke, gas, water leak and similar."
|
|
176009
|
+
),
|
|
176010
|
+
alerts24h: entityList(
|
|
176011
|
+
"24h alerts",
|
|
176012
|
+
"Turned on when a 24h trigger trips the alarm."
|
|
176013
|
+
),
|
|
176014
|
+
alwaysAlerts: entityList(
|
|
176015
|
+
"Always",
|
|
176016
|
+
"The master alert list: fired on every trip in addition to the tier's alerts."
|
|
176017
|
+
),
|
|
176018
|
+
haUrl: {
|
|
176019
|
+
type: "string",
|
|
176020
|
+
title: "Home Assistant URL",
|
|
176021
|
+
description: "e.g. http://homeassistant.local:8123. Set together with the token to use a different Home Assistant; leave both empty to use the bridge's credentials. One without the other is ignored.",
|
|
176022
|
+
required: false
|
|
176023
|
+
},
|
|
176024
|
+
haToken: {
|
|
176025
|
+
type: "string",
|
|
176026
|
+
title: "Long-lived access token",
|
|
176027
|
+
description: "Token for the custom URL, only used together with it. Leave both empty to use the bridge's credentials.",
|
|
176028
|
+
required: false,
|
|
176029
|
+
secret: true
|
|
176030
|
+
}
|
|
176031
|
+
}
|
|
176032
|
+
};
|
|
176033
|
+
}
|
|
176034
|
+
// A watched entity changed state, from the websocket or from tests.
|
|
176035
|
+
handleTriggerEvent(entityId, state, deviceClass) {
|
|
176036
|
+
if (state !== "on" && state !== "open") return;
|
|
176037
|
+
if (!this.watched.has(entityId)) return;
|
|
176038
|
+
this.machine?.handleEntityOn(
|
|
176039
|
+
entityId,
|
|
176040
|
+
isPerimeterTrigger(entityId, deviceClass)
|
|
176041
|
+
);
|
|
176042
|
+
}
|
|
176043
|
+
applyLists() {
|
|
176044
|
+
this.lists = resolveSecurityLists(
|
|
176045
|
+
this.config,
|
|
176046
|
+
(message) => this.log.warn(message)
|
|
176047
|
+
);
|
|
176048
|
+
this.watched = watchedTriggerEntities(this.lists);
|
|
176049
|
+
}
|
|
176050
|
+
machineConfig() {
|
|
176051
|
+
const seconds = (value, fallback) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : fallback;
|
|
176052
|
+
return {
|
|
176053
|
+
exitDelaySeconds: seconds(this.config.exitDelaySeconds, 60),
|
|
176054
|
+
entryDelaySeconds: seconds(this.config.entryDelaySeconds, 60),
|
|
176055
|
+
triggerTimeSeconds: seconds(this.config.triggerTimeSeconds, 120),
|
|
176056
|
+
triggers: this.lists.triggers,
|
|
176057
|
+
triggers24h: this.lists.triggers24h
|
|
176058
|
+
};
|
|
176059
|
+
}
|
|
176060
|
+
effects() {
|
|
176061
|
+
return {
|
|
176062
|
+
switchStates: (states) => {
|
|
176063
|
+
for (const mode of ARM_MODES) {
|
|
176064
|
+
this.context?.updateDeviceState(MODE_DEVICE_IDS[mode], "onOff", {
|
|
176065
|
+
onOff: states[mode]
|
|
176066
|
+
});
|
|
176067
|
+
}
|
|
176068
|
+
},
|
|
176069
|
+
modeReached: (mode) => {
|
|
176070
|
+
this.enqueueSetters(mode);
|
|
176071
|
+
},
|
|
176072
|
+
disarmed: () => {
|
|
176073
|
+
this.enqueueSetters("off");
|
|
176074
|
+
},
|
|
176075
|
+
tripped: (tier, entityId) => {
|
|
176076
|
+
this.log.info(`alarm tripped by ${entityId} (${tier})`);
|
|
176077
|
+
this.context?.updateDeviceState(ALARM_DEVICE_ID, "booleanState", {
|
|
176078
|
+
stateValue: false
|
|
176079
|
+
});
|
|
176080
|
+
const alerts = alertsForTier(this.lists, tier);
|
|
176081
|
+
this.activeAlerts = [.../* @__PURE__ */ new Set([...this.activeAlerts, ...alerts])];
|
|
176082
|
+
this.enqueueAlerts(alerts);
|
|
176083
|
+
},
|
|
176084
|
+
alarmCleared: () => {
|
|
176085
|
+
this.context?.updateDeviceState(ALARM_DEVICE_ID, "booleanState", {
|
|
176086
|
+
stateValue: true
|
|
176087
|
+
});
|
|
176088
|
+
const toSilence = this.activeAlerts.filter(
|
|
176089
|
+
(id) => SILENCEABLE_ALERT_DOMAINS.has(id.split(".")[0])
|
|
176090
|
+
);
|
|
176091
|
+
this.activeAlerts = [];
|
|
176092
|
+
this.enqueueSilence(toSilence);
|
|
176093
|
+
},
|
|
176094
|
+
persist: (snapshot) => {
|
|
176095
|
+
const state = {
|
|
176096
|
+
...snapshot,
|
|
176097
|
+
activeAlerts: [...this.activeAlerts]
|
|
176098
|
+
};
|
|
176099
|
+
void this.context?.storage.set(STATE_KEY, state).catch((e) => this.log.warn("failed to persist alarm state:", e));
|
|
176100
|
+
}
|
|
176101
|
+
};
|
|
176102
|
+
}
|
|
176103
|
+
setters(key) {
|
|
176104
|
+
return key === "off" ? this.lists.offSetters : this.lists.setters[key];
|
|
176105
|
+
}
|
|
176106
|
+
// Every alert a clear can turn off, across all tiers.
|
|
176107
|
+
allSilenceableAlerts() {
|
|
176108
|
+
const all = /* @__PURE__ */ new Set([
|
|
176109
|
+
...ARM_MODES.flatMap((mode) => this.lists.alerts[mode]),
|
|
176110
|
+
...this.lists.alerts24h,
|
|
176111
|
+
...this.lists.alwaysAlerts
|
|
176112
|
+
]);
|
|
176113
|
+
return [...all].filter(
|
|
176114
|
+
(id) => SILENCEABLE_ALERT_DOMAINS.has(id.split(".")[0])
|
|
176115
|
+
);
|
|
176116
|
+
}
|
|
176117
|
+
async registerDevices() {
|
|
176118
|
+
const context = this.context;
|
|
176119
|
+
if (!context) return;
|
|
176120
|
+
for (const mode of ARM_MODES) {
|
|
176121
|
+
await context.registerDevice({
|
|
176122
|
+
id: MODE_DEVICE_IDS[mode],
|
|
176123
|
+
name: MODE_NAMES[mode],
|
|
176124
|
+
deviceType: "on_off_plugin_unit",
|
|
176125
|
+
clusters: [{ clusterId: "onOff", attributes: { onOff: false } }],
|
|
176126
|
+
onAttributeWrite: async (clusterId3, attribute, value) => {
|
|
176127
|
+
if (clusterId3 !== "onOff" || attribute !== "onOff") return;
|
|
176128
|
+
this.machine?.handleModeSwitch(mode, value === true);
|
|
176129
|
+
}
|
|
176130
|
+
});
|
|
176131
|
+
}
|
|
176132
|
+
await context.registerDevice({
|
|
176133
|
+
id: ALARM_DEVICE_ID,
|
|
176134
|
+
name: "Alarm",
|
|
176135
|
+
deviceType: "contact_sensor",
|
|
176136
|
+
// stateValue true = closed = all quiet; false = open = tripped.
|
|
176137
|
+
clusters: [
|
|
176138
|
+
{ clusterId: "booleanState", attributes: { stateValue: true } }
|
|
176139
|
+
]
|
|
176140
|
+
});
|
|
176141
|
+
}
|
|
176142
|
+
// Reflect the restored snapshot onto the endpoints.
|
|
176143
|
+
pushDeviceStates() {
|
|
176144
|
+
const context = this.context;
|
|
176145
|
+
const machine = this.machine;
|
|
176146
|
+
if (!context || !machine) return;
|
|
176147
|
+
const snap = machine.snapshot;
|
|
176148
|
+
for (const mode of ARM_MODES) {
|
|
176149
|
+
context.updateDeviceState(MODE_DEVICE_IDS[mode], "onOff", {
|
|
176150
|
+
onOff: snap.mode === mode && snap.phase !== "disarmed"
|
|
176151
|
+
});
|
|
176152
|
+
}
|
|
176153
|
+
context.updateDeviceState(ALARM_DEVICE_ID, "booleanState", {
|
|
176154
|
+
stateValue: snap.phase !== "triggered"
|
|
176155
|
+
});
|
|
176156
|
+
}
|
|
176157
|
+
pushTask(task) {
|
|
176158
|
+
this.tasks.push(task);
|
|
176159
|
+
queueMicrotask(() => void this.drain());
|
|
176160
|
+
}
|
|
176161
|
+
enqueueSetters(mode) {
|
|
176162
|
+
this.tasks = this.tasks.filter((t) => t.kind !== "setters");
|
|
176163
|
+
this.pushTask({ kind: "setters", mode });
|
|
176164
|
+
}
|
|
176165
|
+
enqueueAlerts(entityIds) {
|
|
176166
|
+
this.dropQueuedIntent("silence", entityIds);
|
|
176167
|
+
const queued = this.tasks.find((t) => t.kind === "alerts");
|
|
176168
|
+
if (queued && queued.kind === "alerts") {
|
|
176169
|
+
queued.entities = [.../* @__PURE__ */ new Set([...queued.entities, ...entityIds])];
|
|
176170
|
+
return;
|
|
176171
|
+
}
|
|
176172
|
+
this.pushTask({ kind: "alerts", entities: [...entityIds] });
|
|
176173
|
+
}
|
|
176174
|
+
enqueueSilence(entityIds) {
|
|
176175
|
+
this.dropQueuedIntent("alerts", entityIds);
|
|
176176
|
+
const queued = this.tasks.find((t) => t.kind === "silence");
|
|
176177
|
+
if (queued && queued.kind === "silence") {
|
|
176178
|
+
queued.entities = [.../* @__PURE__ */ new Set([...queued.entities, ...entityIds])];
|
|
176179
|
+
return;
|
|
176180
|
+
}
|
|
176181
|
+
this.pushTask({ kind: "silence", entities: [...entityIds] });
|
|
176182
|
+
}
|
|
176183
|
+
enqueueReconnect() {
|
|
176184
|
+
if (this.tasks.some((t) => t.kind === "reconnect")) return;
|
|
176185
|
+
this.pushTask({ kind: "reconnect" });
|
|
176186
|
+
}
|
|
176187
|
+
// A newer turn_on/turn_off intent for an entity replaces the queued
|
|
176188
|
+
// opposite one. Only queued tasks are touched, an in-flight batch keeps
|
|
176189
|
+
// its order guarantee.
|
|
176190
|
+
dropQueuedIntent(kind, entityIds) {
|
|
176191
|
+
const drop = new Set(entityIds);
|
|
176192
|
+
this.tasks = this.tasks.flatMap((t) => {
|
|
176193
|
+
if (t.kind !== kind) return [t];
|
|
176194
|
+
const rest = t.entities.filter((id) => !drop.has(id));
|
|
176195
|
+
return rest.length > 0 ? [{ ...t, entities: rest }] : [];
|
|
176196
|
+
});
|
|
176197
|
+
}
|
|
176198
|
+
async drain() {
|
|
176199
|
+
if (this.draining) return;
|
|
176200
|
+
this.draining = true;
|
|
176201
|
+
try {
|
|
176202
|
+
for (; ; ) {
|
|
176203
|
+
const task = this.tasks.shift();
|
|
176204
|
+
if (!task) return;
|
|
176205
|
+
try {
|
|
176206
|
+
await this.runTask(task);
|
|
176207
|
+
} catch (e) {
|
|
176208
|
+
this.log.warn("effect batch failed:", e);
|
|
176209
|
+
}
|
|
176210
|
+
}
|
|
176211
|
+
} finally {
|
|
176212
|
+
this.draining = false;
|
|
176213
|
+
}
|
|
176214
|
+
}
|
|
176215
|
+
async runTask(task) {
|
|
176216
|
+
switch (task.kind) {
|
|
176217
|
+
case "setters":
|
|
176218
|
+
return this.runSetters(task.mode);
|
|
176219
|
+
case "alerts":
|
|
176220
|
+
return this.runAlerts(task.entities);
|
|
176221
|
+
case "silence":
|
|
176222
|
+
return this.runSilence(task.entities);
|
|
176223
|
+
case "reconnect":
|
|
176224
|
+
return this.flushAfterConnect();
|
|
176225
|
+
}
|
|
176226
|
+
}
|
|
176227
|
+
// Setter turn_on per domain: script.turn_on for script.*, scene.turn_on for
|
|
176228
|
+
// scene.*, else <domain>.turn_on. The intent goes to disk first and leaves
|
|
176229
|
+
// it only once every call resolved, so a crash, gap or failed call is
|
|
176230
|
+
// replayed on reconnect if the mode is still current.
|
|
176231
|
+
async runSetters(key) {
|
|
176232
|
+
const entities = this.setters(key);
|
|
176233
|
+
if (entities.length === 0) return;
|
|
176234
|
+
const storage2 = this.context?.storage;
|
|
176235
|
+
await storage2?.set(PENDING_SETTERS_KEY, { mode: key });
|
|
176236
|
+
await storage2?.flush?.();
|
|
176237
|
+
if (this.connection?.connected !== true) {
|
|
176238
|
+
this.log.debug(
|
|
176239
|
+
`no Home Assistant connection, ${key} setters wait for the reconnect`
|
|
176240
|
+
);
|
|
176241
|
+
return;
|
|
176242
|
+
}
|
|
176243
|
+
const ok2 = await this.callSequential(entities, "turn_on");
|
|
176244
|
+
if (ok2) await storage2?.delete(PENDING_SETTERS_KEY);
|
|
176245
|
+
}
|
|
176246
|
+
async runAlerts(entityIds) {
|
|
176247
|
+
if (entityIds.length === 0) return;
|
|
176248
|
+
await this.context?.storage.flush?.();
|
|
176249
|
+
const connection = this.connection;
|
|
176250
|
+
if (!connection || !connection.connected) {
|
|
176251
|
+
this.log.warn(
|
|
176252
|
+
`no Home Assistant connection, alerts skipped: ${entityIds.join(",")}`
|
|
176253
|
+
);
|
|
176254
|
+
return;
|
|
176255
|
+
}
|
|
176256
|
+
await this.callSequential(entityIds, "turn_on");
|
|
176257
|
+
}
|
|
176258
|
+
// Every due turn_off goes to storage first and leaves it only once HA
|
|
176259
|
+
// confirmed the call, so a crash or gap can never lose a silence.
|
|
176260
|
+
async runSilence(entityIds) {
|
|
176261
|
+
const storage2 = this.context?.storage;
|
|
176262
|
+
if (storage2 && entityIds.length > 0) {
|
|
176263
|
+
const pending = await storage2.get(PENDING_SILENCE_KEY) ?? [];
|
|
176264
|
+
await storage2.set(PENDING_SILENCE_KEY, [
|
|
176265
|
+
.../* @__PURE__ */ new Set([...pending, ...entityIds])
|
|
176266
|
+
]);
|
|
176267
|
+
await storage2.flush?.();
|
|
176268
|
+
}
|
|
176269
|
+
await this.flushPendingSilence();
|
|
176270
|
+
}
|
|
176271
|
+
async flushPendingSilence() {
|
|
176272
|
+
const storage2 = this.context?.storage;
|
|
176273
|
+
if (!storage2) return;
|
|
176274
|
+
let pending = await storage2.get(PENDING_SILENCE_KEY) ?? [];
|
|
176275
|
+
if (pending.length === 0) return;
|
|
176276
|
+
const connection = this.connection;
|
|
176277
|
+
if (!connection || !connection.connected) {
|
|
176278
|
+
this.log.warn(
|
|
176279
|
+
`no Home Assistant connection, silence stays pending for ${pending.join(",")}`
|
|
176280
|
+
);
|
|
176281
|
+
return;
|
|
176282
|
+
}
|
|
176283
|
+
for (const entityId of [...pending]) {
|
|
176284
|
+
const domain = entityId.split(".")[0];
|
|
176285
|
+
try {
|
|
176286
|
+
await this.callWithDeadline(connection, domain, "turn_off", entityId);
|
|
176287
|
+
pending = pending.filter((id) => id !== entityId);
|
|
176288
|
+
await storage2.set(PENDING_SILENCE_KEY, pending);
|
|
176289
|
+
} catch (e) {
|
|
176290
|
+
this.log.warn(
|
|
176291
|
+
`${domain}.turn_off failed for ${entityId}, retried on reconnect:`,
|
|
176292
|
+
e
|
|
176293
|
+
);
|
|
176294
|
+
}
|
|
176295
|
+
}
|
|
176296
|
+
if (pending.length === 0) await storage2.delete(PENDING_SILENCE_KEY);
|
|
176297
|
+
}
|
|
176298
|
+
// Ran after every (re)connect: sirens first, then any setter batch a gap
|
|
176299
|
+
// swallowed, but only if the machine still stands where it did.
|
|
176300
|
+
async flushAfterConnect() {
|
|
176301
|
+
await this.flushPendingSilence();
|
|
176302
|
+
const storage2 = this.context?.storage;
|
|
176303
|
+
if (!storage2) return;
|
|
176304
|
+
const held = await storage2.get(
|
|
176305
|
+
PENDING_SETTERS_KEY
|
|
176306
|
+
);
|
|
176307
|
+
if (!held) return;
|
|
176308
|
+
const snap = this.machine?.snapshot;
|
|
176309
|
+
const stillCurrent = held.mode === "off" ? snap?.phase === "disarmed" : snap?.mode === held.mode && snap?.phase !== "disarmed";
|
|
176310
|
+
if (!stillCurrent) {
|
|
176311
|
+
await storage2.delete(PENDING_SETTERS_KEY);
|
|
176312
|
+
return;
|
|
176313
|
+
}
|
|
176314
|
+
const ok2 = await this.callSequential(this.setters(held.mode), "turn_on");
|
|
176315
|
+
if (ok2) await storage2.delete(PENDING_SETTERS_KEY);
|
|
176316
|
+
}
|
|
176317
|
+
async callSequential(entityIds, service) {
|
|
176318
|
+
const connection = this.connection;
|
|
176319
|
+
if (!connection || !connection.connected) return false;
|
|
176320
|
+
let allOk = true;
|
|
176321
|
+
for (const entityId of entityIds) {
|
|
176322
|
+
const domain = entityId.split(".")[0];
|
|
176323
|
+
try {
|
|
176324
|
+
await this.callWithDeadline(connection, domain, service, entityId);
|
|
176325
|
+
} catch (e) {
|
|
176326
|
+
allOk = false;
|
|
176327
|
+
this.log.warn(`${domain}.${service} failed for ${entityId}:`, e);
|
|
176328
|
+
}
|
|
176329
|
+
}
|
|
176330
|
+
return allOk;
|
|
176331
|
+
}
|
|
176332
|
+
async callWithDeadline(connection, domain, service, entityId) {
|
|
176333
|
+
let timer;
|
|
176334
|
+
try {
|
|
176335
|
+
await Promise.race([
|
|
176336
|
+
callService2(connection, domain, service, void 0, {
|
|
176337
|
+
entity_id: entityId
|
|
176338
|
+
}),
|
|
176339
|
+
new Promise((_, reject) => {
|
|
176340
|
+
timer = setTimeout(
|
|
176341
|
+
() => reject(
|
|
176342
|
+
new Error(
|
|
176343
|
+
`${domain}.${service} for ${entityId} timed out after ${CALL_DEADLINE_MS}ms`
|
|
176344
|
+
)
|
|
176345
|
+
),
|
|
176346
|
+
CALL_DEADLINE_MS
|
|
176347
|
+
);
|
|
176348
|
+
timer.unref?.();
|
|
176349
|
+
})
|
|
176350
|
+
]);
|
|
176351
|
+
} finally {
|
|
176352
|
+
if (timer) clearTimeout(timer);
|
|
176353
|
+
}
|
|
176354
|
+
}
|
|
176355
|
+
startConnection() {
|
|
176356
|
+
const hasCustomUrl = !!this.config.haUrl;
|
|
176357
|
+
const hasCustomToken = !!this.config.haToken;
|
|
176358
|
+
if (hasCustomUrl !== hasCustomToken) {
|
|
176359
|
+
this.log.warn(
|
|
176360
|
+
"haUrl and haToken must both be set for a custom Home Assistant, using the bridge credentials instead"
|
|
176361
|
+
);
|
|
176362
|
+
}
|
|
176363
|
+
const custom = hasCustomUrl && hasCustomToken;
|
|
176364
|
+
const haUrl = custom ? this.config.haUrl : this.context?.homeAssistant?.url;
|
|
176365
|
+
const haToken = custom ? this.config.haToken : this.context?.homeAssistant?.accessToken;
|
|
176366
|
+
if (!haUrl || !haToken) {
|
|
176367
|
+
this.log.info(
|
|
176368
|
+
"no Home Assistant connection, setters, alerts and triggers stay inactive"
|
|
176369
|
+
);
|
|
176370
|
+
return;
|
|
176371
|
+
}
|
|
176372
|
+
void this.connect(haUrl, haToken);
|
|
176373
|
+
}
|
|
176374
|
+
async connect(haUrl, haToken) {
|
|
176375
|
+
const generation = this.dialGeneration;
|
|
176376
|
+
let connection;
|
|
176377
|
+
try {
|
|
176378
|
+
connection = this.deps.connect ? await this.deps.connect(haUrl, haToken) : await createConnection4({
|
|
176379
|
+
auth: createLongLivedTokenAuth3(haUrl, haToken)
|
|
176380
|
+
});
|
|
176381
|
+
if (generation !== this.dialGeneration) {
|
|
176382
|
+
connection.close();
|
|
176383
|
+
return;
|
|
176384
|
+
}
|
|
176385
|
+
this.connection = connection;
|
|
176386
|
+
this.backoffMs = RETRY_BASE_MS;
|
|
176387
|
+
connection.addEventListener(
|
|
176388
|
+
"disconnected",
|
|
176389
|
+
() => this.log.info("Home Assistant connection lost, waiting for reconnect")
|
|
176390
|
+
);
|
|
176391
|
+
connection.addEventListener("ready", () => this.enqueueReconnect());
|
|
176392
|
+
const unsubscribe = await connection.subscribeEvents(
|
|
176393
|
+
(event3) => this.handleStateChanged(event3),
|
|
176394
|
+
"state_changed"
|
|
176395
|
+
);
|
|
176396
|
+
if (generation !== this.dialGeneration) {
|
|
176397
|
+
return;
|
|
176398
|
+
}
|
|
176399
|
+
this.unsubscribeEvents = unsubscribe;
|
|
176400
|
+
this.log.info(`watching ${this.watched.size} trigger entities`);
|
|
176401
|
+
this.enqueueReconnect();
|
|
176402
|
+
} catch (e) {
|
|
176403
|
+
connection?.close();
|
|
176404
|
+
if (this.connection === connection) this.connection = void 0;
|
|
176405
|
+
if (generation !== this.dialGeneration) return;
|
|
176406
|
+
this.log.warn(
|
|
176407
|
+
`Home Assistant connection failed, retrying in ${this.backoffMs}ms:`,
|
|
176408
|
+
e
|
|
176409
|
+
);
|
|
176410
|
+
const delay = this.backoffMs;
|
|
176411
|
+
this.backoffMs = Math.min(this.backoffMs * 2, RETRY_MAX_MS);
|
|
176412
|
+
this.retryTimer = setTimeout(() => {
|
|
176413
|
+
this.retryTimer = void 0;
|
|
176414
|
+
if (generation !== this.dialGeneration) return;
|
|
176415
|
+
void this.connect(haUrl, haToken);
|
|
176416
|
+
}, delay);
|
|
176417
|
+
this.retryTimer.unref?.();
|
|
176418
|
+
}
|
|
176419
|
+
}
|
|
176420
|
+
async stopConnection() {
|
|
176421
|
+
this.dialGeneration++;
|
|
176422
|
+
if (this.retryTimer) {
|
|
176423
|
+
clearTimeout(this.retryTimer);
|
|
176424
|
+
this.retryTimer = void 0;
|
|
176425
|
+
}
|
|
176426
|
+
const unsubscribe = this.unsubscribeEvents;
|
|
176427
|
+
this.unsubscribeEvents = void 0;
|
|
176428
|
+
if (unsubscribe) {
|
|
176429
|
+
try {
|
|
176430
|
+
await unsubscribe();
|
|
176431
|
+
} catch {
|
|
176432
|
+
}
|
|
176433
|
+
}
|
|
176434
|
+
this.connection?.close();
|
|
176435
|
+
this.connection = void 0;
|
|
176436
|
+
this.backoffMs = RETRY_BASE_MS;
|
|
176437
|
+
}
|
|
176438
|
+
handleStateChanged(event3) {
|
|
176439
|
+
const entityId = event3.data?.entity_id;
|
|
176440
|
+
const newState = event3.data?.new_state;
|
|
176441
|
+
if (!entityId || !newState?.state) return;
|
|
176442
|
+
if (event3.data?.old_state?.state === newState.state) return;
|
|
176443
|
+
this.handleTriggerEvent(
|
|
176444
|
+
entityId,
|
|
176445
|
+
newState.state,
|
|
176446
|
+
newState.attributes?.device_class
|
|
176447
|
+
);
|
|
176448
|
+
}
|
|
176449
|
+
};
|
|
176450
|
+
|
|
175368
176451
|
// src/plugins/builtin/index.ts
|
|
175369
176452
|
var BUILTIN_PLUGINS = [
|
|
175370
|
-
CameraPlugin
|
|
176453
|
+
CameraPlugin,
|
|
176454
|
+
SecurityPlugin
|
|
175371
176455
|
];
|
|
175372
176456
|
|
|
175373
176457
|
// src/services/bridges/bridge-endpoint-manager.ts
|
|
@@ -175796,8 +176880,13 @@ var BridgeEndpointManager = class extends Service {
|
|
|
175796
176880
|
if (behaviorId === "pluginDevice") continue;
|
|
175797
176881
|
const behaviorEvents = allEvents[behaviorId];
|
|
175798
176882
|
if (!behaviorEvents || typeof behaviorEvents !== "object") continue;
|
|
175799
|
-
|
|
175800
|
-
|
|
176883
|
+
const eventNames = /* @__PURE__ */ new Set();
|
|
176884
|
+
for (let proto = behaviorEvents; proto && proto !== Object.prototype; proto = Object.getPrototypeOf(proto)) {
|
|
176885
|
+
for (const name of Object.getOwnPropertyNames(proto)) {
|
|
176886
|
+
if (name.endsWith("$Changed")) eventNames.add(name);
|
|
176887
|
+
}
|
|
176888
|
+
}
|
|
176889
|
+
for (const eventName of eventNames) {
|
|
175801
176890
|
const observable = behaviorEvents[eventName];
|
|
175802
176891
|
if (!observable || typeof observable.on !== "function") continue;
|
|
175803
176892
|
const attrName = eventName.slice(0, -"$Changed".length);
|
|
@@ -175910,8 +176999,8 @@ var BridgeEndpointManager = class extends Service {
|
|
|
175910
176999
|
async isolateEntity(entityName) {
|
|
175911
177000
|
const endpoints = this.root.parts.map((p) => p);
|
|
175912
177001
|
const endpoint = endpoints.find(
|
|
175913
|
-
(e) => e.id === entityName || e.entityId === entityName
|
|
175914
|
-
);
|
|
177002
|
+
(e) => !(e instanceof VacuumAreaSwitchEndpoint) && (e.id === entityName || e.entityId === entityName)
|
|
177003
|
+
) ?? endpoints.find((e) => e.id === entityName);
|
|
175915
177004
|
if (endpoint) {
|
|
175916
177005
|
this.log.warn(
|
|
175917
177006
|
`Isolating entity ${endpoint.entityId} due to runtime error`
|
|
@@ -175923,6 +177012,17 @@ var BridgeEndpointManager = class extends Service {
|
|
|
175923
177012
|
}
|
|
175924
177013
|
this.pendingRemovals.delete(endpoint.entityId);
|
|
175925
177014
|
this.mappingFingerprints.delete(endpoint.entityId);
|
|
177015
|
+
if (!(endpoint instanceof VacuumAreaSwitchEndpoint)) {
|
|
177016
|
+
for (const sw of endpoints) {
|
|
177017
|
+
if (!(sw instanceof VacuumAreaSwitchEndpoint)) continue;
|
|
177018
|
+
if (sw.vacuumEndpointId !== endpoint.id) continue;
|
|
177019
|
+
try {
|
|
177020
|
+
await sw.delete();
|
|
177021
|
+
} catch (e) {
|
|
177022
|
+
this.log.warn(`Failed to remove area switch ${sw.id}:`, e);
|
|
177023
|
+
}
|
|
177024
|
+
}
|
|
177025
|
+
}
|
|
175926
177026
|
}
|
|
175927
177027
|
}
|
|
175928
177028
|
// refreshDevices only runs on registry-fingerprint changes, which may not
|
|
@@ -176008,7 +177108,7 @@ var BridgeEndpointManager = class extends Service {
|
|
|
176008
177108
|
async refreshDevices() {
|
|
176009
177109
|
this.registry.refresh();
|
|
176010
177110
|
this._failedEntities = [];
|
|
176011
|
-
const endpoints = this.root.parts.map((p) => p);
|
|
177111
|
+
const endpoints = this.root.parts.map((p) => p).filter((p) => !(p instanceof VacuumAreaSwitchEndpoint));
|
|
176012
177112
|
this.entityIds = this.registry.entityIds;
|
|
176013
177113
|
if (this.registry.isAutoComposedDevicesEnabled()) {
|
|
176014
177114
|
for (const eid of this.entityIds) {
|
|
@@ -176071,6 +177171,19 @@ var BridgeEndpointManager = class extends Service {
|
|
|
176071
177171
|
this.bridgeId,
|
|
176072
177172
|
buildPresentEntityIds(fullEntities)
|
|
176073
177173
|
);
|
|
177174
|
+
for (const part of [...this.root.parts]) {
|
|
177175
|
+
if (!(part instanceof VacuumAreaSwitchEndpoint)) continue;
|
|
177176
|
+
const claimant = endpointIdToEntity.get(part.id);
|
|
177177
|
+
if (claimant == null) continue;
|
|
177178
|
+
this.log.info(
|
|
177179
|
+
`Area switch ${part.id} collides with entity ${claimant}, removing the switch`
|
|
177180
|
+
);
|
|
177181
|
+
try {
|
|
177182
|
+
await part.delete();
|
|
177183
|
+
} catch (e) {
|
|
177184
|
+
this.log.warn(`Failed to remove colliding area switch ${part.id}:`, e);
|
|
177185
|
+
}
|
|
177186
|
+
}
|
|
176074
177187
|
const existingEndpoints = [];
|
|
176075
177188
|
const now = Date.now();
|
|
176076
177189
|
for (const endpoint of endpoints) {
|
|
@@ -176230,10 +177343,117 @@ var BridgeEndpointManager = class extends Service {
|
|
|
176230
177343
|
}
|
|
176231
177344
|
}
|
|
176232
177345
|
}
|
|
177346
|
+
await this.reconcileAreaSwitches();
|
|
176233
177347
|
if (this.unsubscribe) {
|
|
176234
177348
|
this.startObserving();
|
|
176235
177349
|
}
|
|
176236
177350
|
}
|
|
177351
|
+
// Opt-in per-area room switches (#355). One momentary OnOffPlugInUnit sibling
|
|
177352
|
+
// per configured service area, mounted alongside its vacuum with a stable
|
|
177353
|
+
// derived id. Areas and mapping come from the parent's vacuumEffective, the
|
|
177354
|
+
// exact config its ServiceArea cluster was built from, never from raw storage
|
|
177355
|
+
// (raw can be a different id space: injected Valetudo/Roborock rooms,
|
|
177356
|
+
// auto-resolved CLEAN_AREA). The vacuum's own endpoint is never touched here.
|
|
177357
|
+
// Switches are kept while their parent endpoint survives with the flag on
|
|
177358
|
+
// (the parent's removal grace is mirrored for free), rebuilt via close() when
|
|
177359
|
+
// the parent was recreated for a mapping change so numbers survive, and
|
|
177360
|
+
// deleted when the flag goes off, the area is gone, or the parent is gone.
|
|
177361
|
+
async reconcileAreaSwitches() {
|
|
177362
|
+
const parts = this.root.parts.map((p) => p);
|
|
177363
|
+
const switches = parts.filter(
|
|
177364
|
+
(p) => p instanceof VacuumAreaSwitchEndpoint
|
|
177365
|
+
);
|
|
177366
|
+
const vacuumById = /* @__PURE__ */ new Map();
|
|
177367
|
+
for (const part of parts) {
|
|
177368
|
+
if (part instanceof VacuumAreaSwitchEndpoint) continue;
|
|
177369
|
+
vacuumById.set(part.id, part);
|
|
177370
|
+
}
|
|
177371
|
+
const isolated = new Set(
|
|
177372
|
+
EntityIsolationService.getIsolatedEntities(this.bridgeId).map(
|
|
177373
|
+
(f) => f.entityId
|
|
177374
|
+
)
|
|
177375
|
+
);
|
|
177376
|
+
const parentIsolated = (parent) => isolated.has(parent.id) || parent.entityId != null && isolated.has(parent.entityId);
|
|
177377
|
+
const kept = /* @__PURE__ */ new Set();
|
|
177378
|
+
for (const sw of switches) {
|
|
177379
|
+
const parent = vacuumById.get(sw.vacuumEndpointId);
|
|
177380
|
+
const mapping = parent ? this.getEntityMapping(parent.entityId) : void 0;
|
|
177381
|
+
const effective = parent instanceof LegacyEndpoint ? parent.vacuumEffective : void 0;
|
|
177382
|
+
let keep = false;
|
|
177383
|
+
let rebuild = false;
|
|
177384
|
+
if (parent && !parentIsolated(parent) && mapping?.vacuumRoomSwitches && effective) {
|
|
177385
|
+
const areas = getVacuumServiceAreas(
|
|
177386
|
+
effective.state.attributes,
|
|
177387
|
+
effective.mapping
|
|
177388
|
+
);
|
|
177389
|
+
if (areas.some((a) => a.areaId === sw.areaId)) {
|
|
177390
|
+
if (sw.parentEffective === effective) {
|
|
177391
|
+
keep = true;
|
|
177392
|
+
} else {
|
|
177393
|
+
rebuild = true;
|
|
177394
|
+
}
|
|
177395
|
+
}
|
|
177396
|
+
}
|
|
177397
|
+
if (keep) {
|
|
177398
|
+
kept.add(sw.id);
|
|
177399
|
+
continue;
|
|
177400
|
+
}
|
|
177401
|
+
try {
|
|
177402
|
+
if (rebuild) {
|
|
177403
|
+
await sw.close();
|
|
177404
|
+
} else {
|
|
177405
|
+
await sw.delete();
|
|
177406
|
+
}
|
|
177407
|
+
} catch (e) {
|
|
177408
|
+
this.log.warn(`Failed to remove area switch ${sw.id}:`, e);
|
|
177409
|
+
}
|
|
177410
|
+
}
|
|
177411
|
+
for (const part of vacuumById.values()) {
|
|
177412
|
+
const entityId = part.entityId;
|
|
177413
|
+
if (!entityId?.startsWith("vacuum.")) continue;
|
|
177414
|
+
if (parentIsolated(part)) continue;
|
|
177415
|
+
const mapping = this.getEntityMapping(entityId);
|
|
177416
|
+
if (!mapping?.vacuumRoomSwitches) continue;
|
|
177417
|
+
const effective = part instanceof LegacyEndpoint ? part.vacuumEffective : void 0;
|
|
177418
|
+
if (!effective) continue;
|
|
177419
|
+
const areas = getVacuumServiceAreas(
|
|
177420
|
+
effective.state.attributes,
|
|
177421
|
+
effective.mapping
|
|
177422
|
+
);
|
|
177423
|
+
const entity = {
|
|
177424
|
+
entity_id: entityId,
|
|
177425
|
+
state: effective.state,
|
|
177426
|
+
registry: this.registry.entity(entityId),
|
|
177427
|
+
deviceRegistry: this.registry.deviceOf(entityId)
|
|
177428
|
+
};
|
|
177429
|
+
for (const area of areas) {
|
|
177430
|
+
const switchId = `${part.id}_roomsw_${area.areaId}`;
|
|
177431
|
+
if (kept.has(switchId)) continue;
|
|
177432
|
+
const holder = vacuumById.get(switchId);
|
|
177433
|
+
if (holder) {
|
|
177434
|
+
this.log.warn(
|
|
177435
|
+
`Skipping area switch ${switchId} for ${entityId}: id taken by entity ${holder.entityId}`
|
|
177436
|
+
);
|
|
177437
|
+
continue;
|
|
177438
|
+
}
|
|
177439
|
+
try {
|
|
177440
|
+
const endpoint = VacuumAreaSwitchEndpoint.create({
|
|
177441
|
+
vacuumEndpointId: part.id,
|
|
177442
|
+
entity,
|
|
177443
|
+
mapping: effective.mapping,
|
|
177444
|
+
area,
|
|
177445
|
+
parentEffective: effective
|
|
177446
|
+
});
|
|
177447
|
+
await this.root.add(endpoint);
|
|
177448
|
+
} catch (e) {
|
|
177449
|
+
this.log.warn(
|
|
177450
|
+
`Failed to add area switch ${switchId} for ${entityId}:`,
|
|
177451
|
+
e
|
|
177452
|
+
);
|
|
177453
|
+
}
|
|
177454
|
+
}
|
|
177455
|
+
}
|
|
177456
|
+
}
|
|
176237
177457
|
updateInFlight;
|
|
176238
177458
|
pendingStates;
|
|
176239
177459
|
pendingChanged;
|
|
@@ -176349,7 +177569,7 @@ var BridgeEndpointManager = class extends Service {
|
|
|
176349
177569
|
init_dist();
|
|
176350
177570
|
init_esm();
|
|
176351
177571
|
init_send_ha_message();
|
|
176352
|
-
import { callService as
|
|
177572
|
+
import { callService as callService3 } from "home-assistant-js-websocket";
|
|
176353
177573
|
import { keys as keys2, pickBy, values as values3 } from "lodash-es";
|
|
176354
177574
|
var BridgeRegistry = class _BridgeRegistry {
|
|
176355
177575
|
constructor(registry3, dataProvider, client) {
|
|
@@ -176735,7 +177955,7 @@ var BridgeRegistry = class _BridgeRegistry {
|
|
|
176735
177955
|
async resolveRoborockRooms(entityId) {
|
|
176736
177956
|
if (!this.client) return [];
|
|
176737
177957
|
try {
|
|
176738
|
-
const raw = await
|
|
177958
|
+
const raw = await callService3(
|
|
176739
177959
|
this.client.connection,
|
|
176740
177960
|
"roborock",
|
|
176741
177961
|
"get_maps",
|