1
0
mirror of https://github.com/home-assistant/frontend.git synced 2026-04-17 23:54:28 +01:00

Improve mock for cover, alarm, lock, lawn mower, valve and vacuum (#29999)

This commit is contained in:
Paul Bottein
2026-03-06 19:41:12 +01:00
committed by GitHub
parent e891ae6bdd
commit 67dbacce52
8 changed files with 429 additions and 14 deletions

View File

@@ -1,4 +1,15 @@
import { MockBaseEntity } from "./base-entity";
import type { EntityAttributes } from "./types";
const TRANSITION_MS = 1000;
const SERVICE_TO_STATE: Record<string, string> = {
alarm_arm_home: "armed_home",
alarm_arm_away: "armed_away",
alarm_arm_night: "armed_night",
alarm_arm_vacation: "armed_vacation",
alarm_arm_custom_bypass: "armed_custom_bypass",
};
export class MockAlarmControlPanelEntity extends MockBaseEntity {
public async handleService(
@@ -10,17 +21,45 @@ export class MockAlarmControlPanelEntity extends MockBaseEntity {
return;
}
const serviceStateMap: Record<string, string> = {
alarm_arm_night: "armed_night",
alarm_arm_home: "armed_home",
alarm_arm_away: "armed_away",
alarm_disarm: "disarmed",
};
this._clearTransition();
if (serviceStateMap[service]) {
this.update({ state: serviceStateMap[service] });
if (service in SERVICE_TO_STATE) {
this._transition("arming", SERVICE_TO_STATE[service], TRANSITION_MS);
return;
}
if (service === "alarm_disarm") {
this.update({ state: "disarmed" });
return;
}
if (service === "alarm_trigger") {
this._transition("pending", "triggered", TRANSITION_MS);
return;
}
super.handleService(domain, service, data);
}
protected _getCapabilityAttributes(): EntityAttributes {
const attrs = this.attributes;
const capabilityAttrs: EntityAttributes = {};
if (attrs.code_format !== undefined) {
capabilityAttrs.code_format = attrs.code_format;
}
if (attrs.code_arm_required !== undefined) {
capabilityAttrs.code_arm_required = attrs.code_arm_required;
}
return capabilityAttrs;
}
protected _getStateAttributes(): EntityAttributes {
const attrs = this.attributes;
return {
changed_by: attrs.changed_by ?? null,
};
}
}

View File

@@ -37,6 +37,8 @@ export class MockBaseEntity {
public hass?: MockHassLike;
private _transitionTimer?: ReturnType<typeof setTimeout>;
static CAPABILITY_ATTRIBUTES: Set<string> = BASE_CAPABILITY_ATTRIBUTES;
constructor(input: EntityInput) {
@@ -76,6 +78,28 @@ export class MockBaseEntity {
);
}
protected _transition(
transitioning: string,
final: string,
duration: number,
onComplete?: () => void
): void {
this._clearTransition();
this.update({ state: transitioning });
this._transitionTimer = setTimeout(() => {
this._transitionTimer = undefined;
this.update({ state: final });
onComplete?.();
}, duration);
}
protected _clearTransition(): void {
if (this._transitionTimer) {
clearTimeout(this._transitionTimer);
this._transitionTimer = undefined;
}
}
public update(changes: {
state?: string;
attributes?: EntityAttributes;

View File

@@ -1,6 +1,14 @@
import { supportsFeatureFromAttributes } from "../../common/entity/supports-feature";
import { CoverEntityFeature } from "../../data/cover";
import { MockBaseEntity } from "./base-entity";
import type { EntityAttributes } from "./types";
const TRANSITION_STEP_MS = 400;
const POSITION_STEP = 10;
export class MockCoverEntity extends MockBaseEntity {
private _positionTimer?: ReturnType<typeof setInterval>;
public async handleService(
domain: string,
service: string,
@@ -11,20 +19,137 @@ export class MockCoverEntity extends MockBaseEntity {
}
if (service === "open_cover") {
this.update({ state: "open" });
this._startTransition(100);
return;
}
if (service === "close_cover") {
this.update({ state: "closed" });
this._startTransition(0);
return;
}
if (service === "toggle") {
if (this.state === "open" || this.state === "opening") {
this._startTransition(0);
} else {
this._startTransition(100);
}
return;
}
if (service === "stop_cover") {
this._stopTransition();
return;
}
if (service === "set_cover_position") {
this._startTransition(data.position);
return;
}
if (service === "open_cover_tilt") {
this.update({ attributes: { current_tilt_position: 100 } });
return;
}
if (service === "close_cover_tilt") {
this.update({ attributes: { current_tilt_position: 0 } });
return;
}
if (service === "stop_cover_tilt") {
return;
}
if (service === "set_cover_tilt_position") {
this.update({
state: data.position > 0 ? "open" : "closed",
attributes: { current_position: data.position },
attributes: { current_tilt_position: data.tilt_position },
});
return;
}
if (service === "toggle_cover_tilt") {
const currentTilt = this.attributes.current_tilt_position ?? 0;
this.update({
attributes: { current_tilt_position: currentTilt > 0 ? 0 : 100 },
});
return;
}
super.handleService(domain, service, data);
}
private _startTransition(targetPosition: number): void {
this._stopTransition();
const hasPosition = supportsFeatureFromAttributes(
this.attributes,
CoverEntityFeature.SET_POSITION
);
if (!hasPosition) {
this.update({ state: targetPosition > 0 ? "open" : "closed" });
return;
}
const currentPosition = this.attributes.current_position ?? 0;
if (currentPosition === targetPosition) {
return;
}
const direction = targetPosition > currentPosition ? 1 : -1;
const transitionState = direction > 0 ? "opening" : "closing";
this.update({ state: transitionState });
this._positionTimer = setInterval(() => {
const pos = this.attributes.current_position ?? 0;
const nextPos = pos + POSITION_STEP * direction;
const reachedTarget =
direction > 0 ? nextPos >= targetPosition : nextPos <= targetPosition;
if (reachedTarget) {
clearInterval(this._positionTimer);
this._positionTimer = undefined;
this.update({
state: targetPosition > 0 ? "open" : "closed",
attributes: { current_position: targetPosition },
});
} else {
this.update({
attributes: { current_position: nextPos },
});
}
}, TRANSITION_STEP_MS);
}
private _stopTransition(): void {
if (this._positionTimer) {
clearInterval(this._positionTimer);
this._positionTimer = undefined;
}
if (this.state === "opening" || this.state === "closing") {
const pos = this.attributes.current_position ?? 0;
this.update({ state: pos > 0 ? "open" : "closed" });
}
}
protected _getStateAttributes(): EntityAttributes {
const attrs = this.attributes;
const stateAttrs: EntityAttributes = {};
if (supportsFeatureFromAttributes(attrs, CoverEntityFeature.SET_POSITION)) {
stateAttrs.current_position = attrs.current_position ?? null;
}
if (
supportsFeatureFromAttributes(attrs, CoverEntityFeature.SET_TILT_POSITION)
) {
stateAttrs.current_tilt_position = attrs.current_tilt_position ?? null;
}
return stateAttrs;
}
}

View File

@@ -0,0 +1,34 @@
import { MockBaseEntity } from "./base-entity";
const TRANSITION_MS = 3000;
export class MockLawnMowerEntity extends MockBaseEntity {
public async handleService(
domain: string,
service: string,
_data: Record<string, any>
): Promise<void> {
if (domain !== this.domain) {
return;
}
this._clearTransition();
if (service === "start_mowing") {
this.update({ state: "mowing" });
return;
}
if (service === "pause") {
this.update({ state: "paused" });
return;
}
if (service === "dock") {
this._transition("returning", "docked", TRANSITION_MS);
return;
}
super.handleService(domain, service, _data);
}
}

View File

@@ -1,4 +1,7 @@
import { MockBaseEntity } from "./base-entity";
import type { EntityAttributes } from "./types";
const TRANSITION_MS = 1000;
export class MockLockEntity extends MockBaseEntity {
public async handleService(
@@ -10,14 +13,44 @@ export class MockLockEntity extends MockBaseEntity {
return;
}
this._clearTransition();
if (service === "lock") {
this.update({ state: "locked" });
this._transition("locking", "locked", TRANSITION_MS);
return;
}
if (service === "unlock") {
this.update({ state: "unlocked" });
this._transition("unlocking", "unlocked", TRANSITION_MS);
return;
}
if (service === "open") {
this._transition("opening", "open", TRANSITION_MS, () => {
this._transition("locking", "unlocked", TRANSITION_MS);
});
return;
}
super.handleService(domain, service, data);
}
protected _getCapabilityAttributes(): EntityAttributes {
const attrs = this.attributes;
const capabilityAttrs: EntityAttributes = {};
if (attrs.code_format !== undefined) {
capabilityAttrs.code_format = attrs.code_format;
}
return capabilityAttrs;
}
protected _getStateAttributes(): EntityAttributes {
const attrs = this.attributes;
return {
changed_by: attrs.changed_by ?? null,
};
}
}

View File

@@ -7,12 +7,15 @@ import { MockFanEntity } from "./fan-entity";
import { MockGroupEntity } from "./group-entity";
import { MockHumidifierEntity } from "./humidifier-entity";
import { MockInputNumberEntity } from "./input-number-entity";
import { MockLawnMowerEntity } from "./lawn-mower-entity";
import { MockInputSelectEntity } from "./input-select-entity";
import { MockInputTextEntity } from "./input-text-entity";
import { MockLightEntity } from "./light-entity";
import { MockLockEntity } from "./lock-entity";
import { MockMediaPlayerEntity } from "./media-player-entity";
import { MockToggleEntity } from "./toggle-entity";
import { MockVacuumEntity } from "./vacuum-entity";
import { MockValveEntity } from "./valve-entity";
import { MockWaterHeaterEntity } from "./water-heater-entity";
type EntityConstructor = new (input: EntityInput) => MockBaseEntity;
@@ -29,10 +32,13 @@ const TYPES: Record<string, EntityConstructor> = {
input_number: MockInputNumberEntity,
input_text: MockInputTextEntity,
input_select: MockInputSelectEntity,
lawn_mower: MockLawnMowerEntity,
light: MockLightEntity,
lock: MockLockEntity,
media_player: MockMediaPlayerEntity,
switch: MockToggleEntity,
vacuum: MockVacuumEntity,
valve: MockValveEntity,
water_heater: MockWaterHeaterEntity,
};

View File

@@ -0,0 +1,88 @@
import { supportsFeatureFromAttributes } from "../../common/entity/supports-feature";
import { VacuumEntityFeature } from "../../data/vacuum";
import { MockBaseEntity } from "./base-entity";
import type { EntityAttributes } from "./types";
const TRANSITION_MS = 3000;
export class MockVacuumEntity extends MockBaseEntity {
public async handleService(
domain: string,
service: string,
data: Record<string, any>
): Promise<void> {
if (domain !== this.domain) {
return;
}
this._clearTransition();
if (service === "start") {
this.update({ state: "cleaning" });
return;
}
if (service === "pause") {
this.update({ state: "paused" });
return;
}
if (service === "stop") {
this.update({ state: "idle" });
return;
}
if (service === "return_to_base") {
this._transition("returning", "docked", TRANSITION_MS);
return;
}
if (service === "clean_spot") {
this.update({ state: "cleaning" });
return;
}
if (service === "locate") {
return;
}
if (service === "set_fan_speed") {
this.update({ attributes: { fan_speed: data.fan_speed } });
return;
}
super.handleService(domain, service, data);
}
protected _getCapabilityAttributes(): EntityAttributes {
const attrs = this.attributes;
const capabilityAttrs: EntityAttributes = {};
if (supportsFeatureFromAttributes(attrs, VacuumEntityFeature.FAN_SPEED)) {
capabilityAttrs.fan_speed_list = attrs.fan_speed_list;
}
return capabilityAttrs;
}
protected _getStateAttributes(): EntityAttributes {
const attrs = this.attributes;
const stateAttrs: EntityAttributes = {};
if (supportsFeatureFromAttributes(attrs, VacuumEntityFeature.FAN_SPEED)) {
stateAttrs.fan_speed = attrs.fan_speed ?? null;
}
if (supportsFeatureFromAttributes(attrs, VacuumEntityFeature.BATTERY)) {
stateAttrs.battery_level = attrs.battery_level ?? null;
stateAttrs.battery_icon = attrs.battery_icon ?? null;
}
if (supportsFeatureFromAttributes(attrs, VacuumEntityFeature.STATUS)) {
stateAttrs.status = attrs.status ?? null;
}
return stateAttrs;
}
}

View File

@@ -0,0 +1,66 @@
import { supportsFeatureFromAttributes } from "../../common/entity/supports-feature";
import { ValveEntityFeature } from "../../data/valve";
import { MockBaseEntity } from "./base-entity";
import type { EntityAttributes } from "./types";
export class MockValveEntity extends MockBaseEntity {
public async handleService(
domain: string,
service: string,
data: Record<string, any>
): Promise<void> {
if (domain !== this.domain) {
return;
}
if (service === "open_valve") {
this.update({
state: "open",
attributes: { current_position: 100 },
});
return;
}
if (service === "close_valve") {
this.update({
state: "closed",
attributes: { current_position: 0 },
});
return;
}
if (service === "toggle") {
if (this.state === "open") {
this.handleService(domain, "close_valve", data);
} else {
this.handleService(domain, "open_valve", data);
}
return;
}
if (service === "stop_valve") {
return;
}
if (service === "set_valve_position") {
this.update({
state: data.position > 0 ? "open" : "closed",
attributes: { current_position: data.position },
});
return;
}
super.handleService(domain, service, data);
}
protected _getStateAttributes(): EntityAttributes {
const attrs = this.attributes;
const stateAttrs: EntityAttributes = {};
if (supportsFeatureFromAttributes(attrs, ValveEntityFeature.SET_POSITION)) {
stateAttrs.current_position = attrs.current_position ?? null;
}
return stateAttrs;
}
}