Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vacuum Topic auto detection and Generate Predefined Zones capability #685

Open
wants to merge 13 commits into
base: dev
Choose a base branch
from
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "xiaomi-vacuum-map-card",
"version": "v2.2.2",
"version": "2.2.3-beta.5",
"description": "Xiaomi Vacuum Map Card",
"keywords": [
"home-assistant",
Expand All @@ -15,12 +15,13 @@
"author": "Piotr Machowski <[email protected]>",
"license": "MIT",
"dependencies": {
"change-perspective": "^1.0.1",
"custom-card-helpers": "^1.9.0",
"home-assistant-js-websocket": "^8.0.1",
"lit": "^2.0.0",
"pointer-tracker": "^2.4.0",
"transformation-matrix": "^2.8.0",
"change-perspective": "^1.0.1"
"tslib": "^2.6.2"
},
"devDependencies": {
"@babel/core": "^7.15.0",
Expand All @@ -43,8 +44,8 @@
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-serve": "^1.1.0",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.30.0",
"typescript": "^4.4.3"
"rollup-plugin-typescript2": "^0.31.0",
"typescript": "^4.9.5"
},
"scripts": {
"start": "rollup -c rollup.config.dev.js --watch",
Expand Down
4 changes: 3 additions & 1 deletion src/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export const EVENT_AUTOGENERATED_CONFIG_GET = "map-card-autogenerated-config-get
export const EVENT_AUTOGENERATED_CONFIG = "map-card-autogenerated-config";
export const EVENT_ROOM_CONFIG_GET = "map-card-room-config-get";
export const EVENT_ROOM_CONFIG = "map-card-room-config";
export const EVENT_PREDEFINED_ZONE_CONFIG_GET = "map-card-predefined-zone-config-get";
export const EVENT_PREDEFINED_ZONE_CONFIG = "map-card-predefined-zone-config";
export const EVENT_SERVICE_CALL_GET = "map-card-service-call-get";
export const EVENT_SERVICE_CALL = "map-card-service-call";
export const EVENT_LOVELACE_DOM = "ll-custom";
Expand All @@ -23,4 +25,4 @@ export const EMPTY_MAP_MODE: MapModeConfig = {
run_immediately: true,
selection_type: SelectionType[SelectionType.ROOM],
repeats_type: RepeatsType[RepeatsType.NONE]
}
}
62 changes: 59 additions & 3 deletions src/editor.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";

Check warning on line 3 in src/editor.ts

View workflow job for this annotation

GitHub Actions / Build

'customElement' is defined but never used

Check warning on line 3 in src/editor.ts

View workflow job for this annotation

GitHub Actions / Build

'property' is defined but never used

Check warning on line 3 in src/editor.ts

View workflow job for this annotation

GitHub Actions / Build

'state' is defined but never used
import { fireEvent, LovelaceCardEditor } from "custom-card-helpers";

import { RoomConfigEventData, TranslatableString, XiaomiVacuumMapCardConfig } from "./types/types";
import { RoomConfigEventData, PredefinedZoneConfigEventData, TranslatableString, XiaomiVacuumMapCardConfig } from "./types/types";
import { localizeWithHass } from "./localize/localize";
import { PlatformGenerator } from "./model/generators/platform-generator";
import {
CARD_VERSION,
EDITOR_CUSTOM_ELEMENT_NAME,

Check warning on line 11 in src/editor.ts

View workflow job for this annotation

GitHub Actions / Build

'EDITOR_CUSTOM_ELEMENT_NAME' is defined but never used
EVENT_AUTOGENERATED_CONFIG,
EVENT_AUTOGENERATED_CONFIG_GET,
EVENT_ROOM_CONFIG,
EVENT_ROOM_CONFIG_GET,
EVENT_PREDEFINED_ZONE_CONFIG,
EVENT_PREDEFINED_ZONE_CONFIG_GET,
EVENT_SELECTION_CHANGED,
EVENT_SERVICE_CALL,
EVENT_SERVICE_CALL_GET,
Expand All @@ -22,7 +24,7 @@
import { HomeAssistantFixed } from "./types/fixes";

@customElement(EDITOR_CUSTOM_ELEMENT_NAME)
export class XiaomiVacuumMapCardEditor extends LitElement implements Omit<LovelaceCardEditor, "hass"> {

Check warning on line 27 in src/editor.ts

View workflow job for this annotation

GitHub Actions / Build

'XiaomiVacuumMapCardEditor' is defined but never used
@property({ attribute: false }) public hass?: HomeAssistantFixed;
@state() private _config?: XiaomiVacuumMapCardConfig;
@state() private _helpers?: any;
Expand All @@ -34,6 +36,7 @@
this._handleNewSelection = this._handleNewSelection.bind(this);
this._handleAutogeneratedConfig = this._handleAutogeneratedConfig.bind(this);
this._handleRoomConfig = this._handleRoomConfig.bind(this);
this._handlePredefinedZoneConfig = this._handlePredefinedZoneConfig.bind(this);
this._handleServiceCall = this._handleServiceCall.bind(this);
}

Expand Down Expand Up @@ -75,6 +78,10 @@
window.dispatchEvent(new Event(EVENT_ROOM_CONFIG_GET));
}

private static _generateZonesConfig(): void {
window.dispatchEvent(new Event(EVENT_PREDEFINED_ZONE_CONFIG_GET));
}

public setConfig(config: XiaomiVacuumMapCardConfig): void {
this._config = config;
this.loadCardHelpers();
Expand All @@ -85,6 +92,7 @@
window.addEventListener(EVENT_SELECTION_CHANGED, this._handleNewSelection);
window.addEventListener(EVENT_AUTOGENERATED_CONFIG, this._handleAutogeneratedConfig);
window.addEventListener(EVENT_ROOM_CONFIG, this._handleRoomConfig);
window.addEventListener(EVENT_PREDEFINED_ZONE_CONFIG, this._handlePredefinedZoneConfig);
window.addEventListener(EVENT_SERVICE_CALL, this._handleServiceCall);
}

Expand All @@ -93,6 +101,7 @@
window.removeEventListener(EVENT_SELECTION_CHANGED, this._handleNewSelection);
window.removeEventListener(EVENT_AUTOGENERATED_CONFIG, this._handleAutogeneratedConfig);
window.removeEventListener(EVENT_ROOM_CONFIG, this._handleRoomConfig);
window.removeEventListener(EVENT_PREDEFINED_ZONE_CONFIG, this._handlePredefinedZoneConfig);
window.removeEventListener(EVENT_SERVICE_CALL, this._handleServiceCall);
}

Expand All @@ -111,12 +120,15 @@
this._helpers.importMoreInfoControl("climate");

const entityIds = Object.keys(this.hass.states);
const cameras = entityIds.filter(e => e.substr(0, e.indexOf(".")) === "camera");
const vacuums = entityIds.filter(e => e.substr(0, e.indexOf(".")) === "vacuum");
const cameras = entityIds.filter(e => e.substring(0, e.indexOf(".")) === "camera");
const vacuums = entityIds.filter(e => e.substring(0, e.indexOf(".")) === "vacuum");
const platforms = PlatformGenerator.getPlatforms();
const roomsUnavailable =
this.hass.states[this._camera]?.attributes?.["rooms"] === undefined ||
PlatformGenerator.getRoomsTemplate(this._vacuum_platform) === undefined;
const zonesUnavailable =
this.hass.states[this._camera]?.attributes?.["zones"] === undefined ||
PlatformGenerator.getPedefinedZonesTemplate(this._vacuum_platform) === undefined;

return html`
<div class="card-config">
Expand Down Expand Up @@ -219,6 +231,11 @@
.disabled=${roomsUnavailable}>
${this._localize("editor.label.generate_rooms_config")}
</mwc-button>
<mwc-button
@click="${() => XiaomiVacuumMapCardEditor._generateZonesConfig()}"
.disabled=${zonesUnavailable}>
${this._localize("editor.label.generate_zones_config")}
</mwc-button>
<mwc-button @click="${() => XiaomiVacuumMapCardEditor._copyServiceCall()}">
${this._localize("editor.label.copy_service_call")}
</mwc-button>
Expand Down Expand Up @@ -280,6 +297,36 @@
this._showToast("editor.label.config_set", "mdi:check", true);
}

private _handlePredefinedZoneConfig(e: Event): void {
const PredefinedZonesTemplate = PlatformGenerator.getPedefinedZonesTemplate(this._vacuum_platform);
const PredefinedZoneConfig = (e as any).PredefinedZoneConfig as PredefinedZoneConfigEventData;
if (!PredefinedZoneConfig) {
this._showToast("editor.label.config_set_failed", "mdi:close", false);
return;
}
const map_modes = this._config?.map_modes ?? [];
if (map_modes.length !== 0 && (PredefinedZoneConfig.modeIndex ?? -1) >= 0) {
map_modes[PredefinedZoneConfig.modeIndex ?? -1] = {
...map_modes[PredefinedZoneConfig.modeIndex ?? -1],
predefined_selections: PredefinedZoneConfig.zones,
};
} else {
if (map_modes.length === 0) {
map_modes.push(...PlatformGenerator.generateDefaultModes(this._vacuum_platform));
}
if (PredefinedZonesTemplate) {
map_modes.push({
template: PredefinedZonesTemplate,
predefined_selections: PredefinedZoneConfig.zones,
});
}
}
if (this._config) {
this._setConfig({ ...this._config, map_modes: map_modes });
}
this._showToast("editor.label.config_set", "mdi:check", true);
}

private _handleServiceCall(e: Event): void {
const serviceCall = (e as any).serviceCall as string;
copyMessage(serviceCall ?? "");
Expand Down Expand Up @@ -311,6 +358,15 @@
if (this._vacuum_platform === value) return;
const tmpConfig = { ...this._config };
tmpConfig["vacuum_platform"] = value;

const tmpCheck = PlatformGenerator.getVariables(tmpConfig.vacuum_platform);
if (tmpCheck && "topic" in tmpCheck) {
const mytopic = this.hass.states[tmpConfig.map_source?.camera ?? ""]?.attributes["vacuum_topic"];
if (mytopic !== undefined) {
tmpConfig["internal_variables"] = { topic: mytopic };
}
}

if (PlatformGenerator.getCalibration(tmpConfig.vacuum_platform)) {
if (tmpConfig["calibration_source"] && tmpConfig["calibration_source"]["camera"]) {
delete tmpConfig["calibration_source"];
Expand Down
1 change: 1 addition & 0 deletions src/localize/languages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@
"config_set": "Config set!\nOpen config editor to adjust it.",
"config_set_failed": "Failed to update config.",
"generate_rooms_config": "Generate rooms config",
"generate_zones_config": "Generate zones config",
"copy_service_call": "Copy service call"
},
"alerts": {
Expand Down
13 changes: 13 additions & 0 deletions src/model/generators/platform-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,18 @@ export class PlatformGenerator {
return undefined;
}

public static getPedefinedZonesTemplate(platform: string): string | undefined {
const platformTemplate = this.getPlatformTemplate(platform);
for (const templateName in platformTemplate.map_modes.templates) {
const template = platformTemplate.map_modes.templates[templateName];
if (template.selection_type === SelectionType[SelectionType.PREDEFINED_RECTANGLE]) {
console.log("got template:", templateName);
return templateName;
}
}
return undefined;
}

public static getCalibration(platform: string | undefined): CalibrationPoint[] | undefined {
return this.getPlatformTemplate(PlatformGenerator.getPlatformName(platform)).calibration_points;
}
Expand All @@ -170,4 +182,5 @@ export class PlatformGenerator {
} as unknown as PlatformTemplate)
);
}

}
7 changes: 5 additions & 2 deletions src/model/generators/platform_templates/hypfer_valetudo.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"internal_variables": {
"topic": "[[topic]]"
},
"map_modes": {
"default_templates": [
"vacuum_clean_zone",
Expand Down Expand Up @@ -40,7 +43,7 @@
"vacuum_clean_zone_predefined": {
"name": "map_mode.vacuum_clean_zone_predefined",
"icon": "mdi:floor-plan",
"selection_type": "ROOM",
"selection_type": "PREDEFINED_RECTANGLE",
"repeats_type": "EXTERNAL",
"max_repeats": 3,
"service_call_schema": {
Expand Down Expand Up @@ -71,7 +74,7 @@
"name": "map_mode.vacuum_goto_predefined",
"icon": "mdi:map-marker",
"max_selections": 1,
"selection_type": "ROOM",
"selection_type": "PREDEFINED_POINT",
"repeats_type": "NONE",
"service_call_schema": {
"service": "mqtt.publish",
Expand Down
10 changes: 7 additions & 3 deletions src/model/generators/platform_templates/rand256_valetudo_re.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"internal_variables": {
"topic": "topic"
},
"map_modes": {
"default_templates": [
"vacuum_clean_zone",
Expand All @@ -8,6 +11,7 @@
"vacuum_clean_segment": {
"name": "map_mode.vacuum_clean_segment",
"icon": "mdi:floor-plan",
"id_type": "number",
"selection_type": "ROOM",
"repeats_type": "EXTERNAL",
"max_repeats": 3,
Expand Down Expand Up @@ -41,15 +45,15 @@
"vacuum_clean_zone_predefined": {
"name": "map_mode.vacuum_clean_zone_predefined",
"icon": "mdi:floor-plan",
"selection_type": "ROOM",
"selection_type": "PREDEFINED_RECTANGLE",
"repeats_type": "EXTERNAL",
"max_repeats": 3,
"service_call_schema": {
"service": "mqtt.publish",
"evaluate_data_as_template": true,
"service_data": {
"topic": "[[topic]]/custom_command",
"payload": "{\"command\": \"zoned_cleanup\",\"zone_ids\": [{%for s in ('[[selection]]')|from_json %}{ \"id\": \"{{s}}\", \"repeats\": [[repeats]]}{%if not loop.last%},{%endif%}{%endfor%}],\"afterCleaning\": \"{{ 'Base' if 'afterCleaning' in '[[afterCleaning]]' else '[[afterCleaning]]'}}\"}"
"payload": "{\"command\": \"zoned_cleanup\",\"zone_coordinates\": [{%for s in ('[[selection]]')|from_json %}{ \"x1\": {{s[0]}}, \"y1\": {{s[1]}}, \"x2\": {{s[2]}}, \"y2\": {{s[3]}}, \"repeats\": [[repeats]]}{%if not loop.last%},{%endif%}{%endfor%}],\"afterCleaning\": \"{{ 'Base' if 'afterCleaning' in '[[afterCleaning]]' else '[[afterCleaning]]'}}\"}"
}
}
},
Expand All @@ -72,7 +76,7 @@
"name": "map_mode.vacuum_goto_predefined",
"icon": "mdi:map-marker",
"max_selections": 1,
"selection_type": "ROOM",
"selection_type": "PREDEFINED_POINT",
"repeats_type": "NONE",
"service_call_schema": {
"service": "mqtt.publish",
Expand Down
14 changes: 14 additions & 0 deletions src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ export interface ConditionConfig {
readonly attribute?: string;
readonly value?: string;
readonly value_not?: string;
readonly topic?: string;
}

export interface CalibrationPoint {
Expand Down Expand Up @@ -282,11 +283,24 @@ export interface MapExtractorRoom {
readonly y: number | undefined;
}

export interface MapExtractorZone {
readonly zones: string | undefined;
readonly name: string | undefined;
readonly icon: string | undefined;
readonly x: number | undefined;
readonly y: number | undefined;
}

export interface RoomConfigEventData {
readonly modeIndex: number;
readonly rooms: Array<RoomConfig>;
}

export interface PredefinedZoneConfigEventData {
readonly modeIndex: number;
readonly zones: Array<PredefinedZoneConfig>;
}

export interface EntityConfig {
entity: string;
attribute?: string;
Expand Down
Loading
Loading