mirror of
https://github.com/PiotrMachowski/lovelace-xiaomi-vacuum-map-card.git
synced 2021-06-25 09:14:01 +03:00
723 lines
27 KiB
JavaScript
723 lines
27 KiB
JavaScript
import CoordinatesConverter from './coordinates-converter.js';
|
|
import style from './style.js';
|
|
import {
|
|
mode,
|
|
goToTarget,
|
|
zonedCleanup,
|
|
zones,
|
|
run,
|
|
repeats,
|
|
confirmation,
|
|
texts
|
|
} from './texts.js'
|
|
|
|
const LitElement = Object.getPrototypeOf(
|
|
customElements.get("ha-panel-lovelace")
|
|
);
|
|
const html = LitElement.prototype.html;
|
|
|
|
if (typeof loadCardHelpers !== "undefined") {
|
|
loadCardHelpers().then(helpers => {
|
|
if (typeof helpers.importMoreInfoControl !== "undefined") {
|
|
helpers.importMoreInfoControl("light");
|
|
}
|
|
});
|
|
}
|
|
|
|
class XiaomiVacuumMapCard extends LitElement {
|
|
constructor() {
|
|
super();
|
|
this.isMouseDown = false;
|
|
this.rectangles = [];
|
|
this.selectedRectangle = -1;
|
|
this.selectedZones = [];
|
|
this.currRectangle = {x: null, y: null, w: null, h: null};
|
|
this.imageScale = -1;
|
|
this.mode = 0;
|
|
this.vacuumZonedCleanupRepeats = 1;
|
|
this.currPoint = {x: null, y: null};
|
|
this.outdatedConfig = false;
|
|
this.missingCameraAttribute = false;
|
|
}
|
|
|
|
static get properties() {
|
|
return {
|
|
_hass: {},
|
|
_config: {},
|
|
isMouseDown: {},
|
|
rectangles: {},
|
|
selectedRectangle: {},
|
|
selectedZones: {},
|
|
currRectangle: {},
|
|
mode: {},
|
|
vacuumZonedCleanupRepeats: {},
|
|
currPoint: {},
|
|
mapDrawing: {},
|
|
};
|
|
}
|
|
|
|
set hass(hass) {
|
|
this._hass = hass;
|
|
if (this._config && !this.map_image) {
|
|
this.updateCameraImage();
|
|
}
|
|
}
|
|
|
|
setConfig(config) {
|
|
const availableModes = new Map();
|
|
this._language = config.language || "en";
|
|
availableModes.set("go_to_target", texts[this._language][goToTarget]);
|
|
availableModes.set("zoned_cleanup", texts[this._language][zonedCleanup]);
|
|
availableModes.set("predefined_zones", texts[this._language][zones]);
|
|
|
|
if (!config.entity) {
|
|
throw new Error("Missing configuration: entity");
|
|
}
|
|
if (!config.map_image && !config.map_camera) {
|
|
throw new Error("Missing configuration: map_image or map_camera");
|
|
}
|
|
if (config.map_image && config.map_camera) {
|
|
throw new Error("Only one of following properties is allowed: map_image or map_camera");
|
|
}
|
|
if (config.base_position || config.reference_point) {
|
|
this.outdatedConfig = true;
|
|
this._config = config;
|
|
return;
|
|
}
|
|
if (!config.camera_calibration) {
|
|
if (!config.calibration_points || !Array.isArray(config.calibration_points)) {
|
|
throw new Error("Missing configuration: calibration_points or camera_calibration");
|
|
}
|
|
if (config.calibration_points.length !== 3) {
|
|
throw new Error("Exactly 3 calibration_points required");
|
|
}
|
|
for (const calibration_point of config.calibration_points) {
|
|
if (calibration_point.map === null) {
|
|
throw new Error("Missing configuration: calibration_points.map");
|
|
}
|
|
if (calibration_point.map.x === null) {
|
|
throw new Error("Missing configuration: calibration_points.map.x");
|
|
}
|
|
if (calibration_point.map.y === null) {
|
|
throw new Error("Missing configuration: calibration_points.map.y");
|
|
}
|
|
if (calibration_point.vacuum === null) {
|
|
throw new Error("Missing configuration: calibration_points.vacuum");
|
|
}
|
|
if (calibration_point.vacuum.x === null) {
|
|
throw new Error("Missing configuration: calibration_points.vacuum.x");
|
|
}
|
|
if (calibration_point.vacuum.y === null) {
|
|
throw new Error("Missing configuration: calibration_points.vacuum.y");
|
|
}
|
|
}
|
|
this.updateCoordinates(config)
|
|
} else {
|
|
if (!config.map_camera) {
|
|
throw new Error("Invalid configuration: map_camera is required for camera_calibration");
|
|
}
|
|
}
|
|
|
|
if (config.modes) {
|
|
if (!Array.isArray(config.modes) || config.modes.length < 1 || config.modes.length > 3) {
|
|
throw new Error("Invalid configuration: modes");
|
|
}
|
|
this.modes = [];
|
|
for (const mode of config.modes) {
|
|
if (!availableModes.has(mode)) {
|
|
throw new Error("Invalid mode: " + mode);
|
|
}
|
|
this.modes.push(availableModes.get(mode));
|
|
}
|
|
} else {
|
|
this.modes = [
|
|
texts[this._language][goToTarget],
|
|
texts[this._language][zonedCleanup],
|
|
texts[this._language][zones]
|
|
];
|
|
}
|
|
if (!config.zones || !Array.isArray(config.zones) || config.zones.length === 0 && this.modes.includes(texts[this._language][zones])) {
|
|
this.modes.splice(this.modes.indexOf(texts[this._language][zones]), 1);
|
|
}
|
|
if (config.default_mode) {
|
|
if (!availableModes.has(config.default_mode) || !this.modes.includes(availableModes.get(config.default_mode))) {
|
|
throw new Error("Invalid default mode: " + config.default_mode);
|
|
}
|
|
this.defaultMode = this.modes.indexOf(availableModes.get(config.default_mode));
|
|
} else {
|
|
this.defaultMode = -1;
|
|
}
|
|
if (config.service && config.service.split(".").length === 2) {
|
|
this.service_domain = config.service.split(".")[0];
|
|
this.service_method = config.service.split(".")[1];
|
|
} else {
|
|
this.service_domain = "vacuum";
|
|
this.service_method = "send_command";
|
|
}
|
|
if (config.map_image) {
|
|
this.map_image = config.map_image;
|
|
}
|
|
this._map_refresh_interval = (config.camera_refresh_interval || 5) * 1000;
|
|
this._config = config;
|
|
}
|
|
|
|
updateCoordinates(config) {
|
|
const p1 = this.getCalibrationPoint(config, 0);
|
|
const p2 = this.getCalibrationPoint(config, 1);
|
|
const p3 = this.getCalibrationPoint(config, 2);
|
|
this.coordinatesConverter = new CoordinatesConverter(p1, p2, p3);
|
|
}
|
|
|
|
getConfigurationMigration(config) {
|
|
const diffX = config.reference_point.x - config.base_position.x;
|
|
const diffY = config.reference_point.y - config.base_position.y;
|
|
const shouldSwapAxis = diffX * diffY > 0;
|
|
let unit = shouldSwapAxis ? diffX : diffY;
|
|
if (shouldSwapAxis) {
|
|
const temp = config.base_position.x;
|
|
config.base_position.x = config.base_position.y;
|
|
config.base_position.y = temp;
|
|
}
|
|
const canvasX = config.base_position.x;
|
|
const canvasY = unit + config.base_position.y;
|
|
let x = Math.round(canvasX);
|
|
let y = Math.round(canvasY);
|
|
if (shouldSwapAxis) {
|
|
x = Math.round(canvasY);
|
|
y = Math.round(canvasX);
|
|
}
|
|
return html`
|
|
<ha-card id="xiaomiCard" style="padding: 16px">
|
|
<div class="card-header" style="padding: 8px 0 16px 0;"><div class="name">Xiaomi Vacuum Map card</div></div>
|
|
<h3>Your configuration is outdated</h3>
|
|
<p>Migrate it using following calibration settings:</p>
|
|
<pre><textarea style="width: 100%; height: 22em">calibration_points:
|
|
- vacuum:
|
|
x: 25500
|
|
y: 25500
|
|
map:
|
|
x: ${config.base_position.x}
|
|
y: ${config.base_position.y}
|
|
- vacuum:
|
|
x: 26500
|
|
y: 26500
|
|
map:
|
|
x: ${config.reference_point.x}
|
|
y: ${config.reference_point.y}
|
|
- vacuum:
|
|
x: 25500
|
|
y: 26500
|
|
map:
|
|
x: ${x}
|
|
y: ${y}</textarea></pre>
|
|
</ha-card>`
|
|
}
|
|
|
|
getCalibrationPoint(config, index) {
|
|
return {
|
|
a: {
|
|
x: config.calibration_points[index].map.x,
|
|
y: config.calibration_points[index].map.y
|
|
},
|
|
b: {
|
|
x: config.calibration_points[index].vacuum.x,
|
|
y: config.calibration_points[index].vacuum.y
|
|
}
|
|
};
|
|
}
|
|
|
|
render() {
|
|
if (this.outdatedConfig) {
|
|
return this.getConfigurationMigration(this._config);
|
|
}
|
|
const modesDropdown = this.modes.map(m => html`<paper-item>${m}</paper-item>`);
|
|
const rendered = html`
|
|
${style}
|
|
<ha-card id="xiaomiCard">
|
|
<div id="mapWrapper">
|
|
<div id="map">
|
|
<img id="mapBackground" @load="${() => this.calculateScale()}" src="${this.map_image}">
|
|
<canvas id="mapDrawing" style="${this.getCanvasStyle()}"
|
|
@mousemove="${e => this.onMouseMove(e)}"
|
|
@mousedown="${e => this.onMouseDown(e)}"
|
|
@mouseup="${e => this.onMouseUp(e)}"
|
|
@touchstart="${e => this.onTouchStart(e)}"
|
|
@touchend="${e => this.onTouchEnd(e)}"
|
|
@touchmove="${e => this.onTouchMove(e)}" />
|
|
</div>
|
|
</div>
|
|
${this.missingCameraAttribute ?
|
|
html`<div style="padding: 5px;">
|
|
<h3>Your camera entity is not providing calibration_points</h3>
|
|
<p>Enable calibration_points in camera entity or disable camera_calibration</p>
|
|
</div>` :
|
|
html`<div class="dropdownWrapper">
|
|
<paper-dropdown-menu label="${texts[this._language][mode]}" @value-changed="${e => this.modeSelected(e)}" class="vacuumDropdown" selected="${this.defaultMode}">
|
|
<paper-listbox slot="dropdown-content" class="dropdown-content" selected="${this.defaultMode}">
|
|
${modesDropdown}
|
|
</paper-listbox>
|
|
</paper-dropdown-menu>
|
|
</div>
|
|
<p class="buttonsWrapper">
|
|
<span id="increaseButton" hidden><mwc-button @click="${() => this.vacuumZonedIncreaseButton()}">${texts[this._language][repeats]} ${this.vacuumZonedCleanupRepeats}</mwc-button></span>
|
|
<mwc-button class="vacuumRunButton" @click="${() => this.vacuumStartButton(true)}">${texts[this._language][run]}</mwc-button>
|
|
</p>
|
|
<div id="toast"><div id="img"><ha-icon icon="mdi:check" style="vertical-align: center"></ha-icon></div><div id="desc">${texts[this._language][confirmation]}</div></div>`}
|
|
</ha-card>`;
|
|
if (this.getMapImage()) {
|
|
this.calculateScale();
|
|
}
|
|
return rendered;
|
|
}
|
|
|
|
calculateScale() {
|
|
const img = this.getMapImage();
|
|
const canvas = this.getCanvas();
|
|
this.imageScale = img.width / img.naturalWidth;
|
|
const mapHeight = Math.round(this.imageScale * img.naturalHeight);
|
|
img.parentElement.parentElement.style.height = mapHeight + "px";
|
|
canvas.width = img.width;
|
|
canvas.height = mapHeight;
|
|
this.drawCanvas();
|
|
}
|
|
|
|
onMouseDown(e) {
|
|
const pos = this.getMousePos(e);
|
|
this.isMouseDown = true;
|
|
if (this.mode === 1) {
|
|
this.currPoint.x = pos.x;
|
|
this.currPoint.y = pos.y;
|
|
} else if (this.mode === 2) {
|
|
const {selected, shouldDelete, shouldResize} = this.getSelectedRectangle(pos.x, pos.y);
|
|
this.currRectangle.x = pos.x;
|
|
this.currRectangle.y = pos.y;
|
|
if (shouldDelete) {
|
|
this.rectangles.splice(selected, 1);
|
|
this.selectedRectangle = -1;
|
|
this.isMouseDown = false;
|
|
this.drawCanvas();
|
|
return;
|
|
}
|
|
if (shouldResize) {
|
|
this.currRectangle.x = this.rectangles[selected].x;
|
|
this.currRectangle.y = this.rectangles[selected].y;
|
|
this.rectangles.splice(selected, 1);
|
|
this.drawCanvas();
|
|
return;
|
|
}
|
|
this.selectedRectangle = selected;
|
|
if (this.selectedRectangle >= 0) {
|
|
this.currRectangle.w = this.rectangles[this.selectedRectangle].x;
|
|
this.currRectangle.h = this.rectangles[this.selectedRectangle].y;
|
|
} else {
|
|
this.currRectangle.w = 0;
|
|
this.currRectangle.h = 0;
|
|
}
|
|
} else if (this.mode === 3) {
|
|
const selectedZone = this.getSelectedZone(pos.x, pos.y);
|
|
if (selectedZone >= 0) {
|
|
if (this.selectedZones.includes(selectedZone)) {
|
|
this.selectedZones.splice(this.selectedZones.indexOf(selectedZone), 1);
|
|
} else {
|
|
if (this.selectedZones.length < 5 || this._config.ignore_zones_limit) {
|
|
this.selectedZones.push(selectedZone);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
this.drawCanvas();
|
|
}
|
|
|
|
onMouseUp(e) {
|
|
this.isMouseDown = false;
|
|
if (this.selectedRectangle >= 0 || this.mode !== 2 || this.mode === 2 && this.rectangles.length >= 5 && !this._config.ignore_zones_limit) {
|
|
this.selectedRectangle = -1;
|
|
this.drawCanvas();
|
|
return;
|
|
}
|
|
const {x, y} = this.getMousePos(e);
|
|
const rx = Math.min(x, this.currRectangle.x);
|
|
const ry = Math.min(y, this.currRectangle.y);
|
|
const rw = Math.max(x, this.currRectangle.x) - rx;
|
|
const rh = Math.max(y, this.currRectangle.y) - ry;
|
|
this.currRectangle.x = rx;
|
|
this.currRectangle.y = ry;
|
|
this.currRectangle.w = rw;
|
|
this.currRectangle.h = rh;
|
|
if (rw > 5 && rh > 5) {
|
|
this.rectangles.push({x: rx, y: ry, w: rw, h: rh});
|
|
}
|
|
this.drawCanvas();
|
|
}
|
|
|
|
onMouseMove(e) {
|
|
if (this.isMouseDown && this.mode === 2) {
|
|
const {x, y} = this.getMousePos(e);
|
|
if (this.selectedRectangle < 0) {
|
|
this.currRectangle.w = x - this.currRectangle.x;
|
|
this.currRectangle.h = y - this.currRectangle.y;
|
|
} else {
|
|
this.rectangles[this.selectedRectangle].x = this.currRectangle.w + x - this.currRectangle.x;
|
|
this.rectangles[this.selectedRectangle].y = this.currRectangle.h + y - this.currRectangle.y;
|
|
}
|
|
this.drawCanvas();
|
|
}
|
|
}
|
|
|
|
onTouchStart(e) {
|
|
if (this.mode === 2) {
|
|
this.onMouseDown(this.convertTouchToMouse(e));
|
|
}
|
|
}
|
|
|
|
onTouchEnd(e) {
|
|
if (this.mode === 2) {
|
|
this.onMouseUp(this.convertTouchToMouse(e));
|
|
}
|
|
}
|
|
|
|
onTouchMove(e) {
|
|
if (this.mode === 2) {
|
|
this.onMouseMove(this.convertTouchToMouse(e));
|
|
}
|
|
}
|
|
|
|
modeSelected(e) {
|
|
const selected = e.detail.value;
|
|
this.mode = 0;
|
|
if (selected === texts[this._language][goToTarget]) {
|
|
this.mode = 1;
|
|
} else if (selected === texts[this._language][zonedCleanup]) {
|
|
this.mode = 2;
|
|
} else if (selected === texts[this._language][zones]) {
|
|
this.mode = 3;
|
|
}
|
|
this.getPredefinedZonesIncreaseButton().hidden = this.mode !== 3 && this.mode !== 2;
|
|
this.drawCanvas();
|
|
}
|
|
|
|
vacuumZonedIncreaseButton() {
|
|
this.vacuumZonedCleanupRepeats++;
|
|
if (this.vacuumZonedCleanupRepeats > 3) {
|
|
this.vacuumZonedCleanupRepeats = 1;
|
|
}
|
|
}
|
|
|
|
vacuumStartButton(debug) {
|
|
if (this.mode === 1 && this.currPoint.x != null) {
|
|
this.vacuumGoToPoint(debug);
|
|
} else if (this.mode === 2 && !this.rectangles.empty) {
|
|
this.vacuumStartZonedCleanup(debug);
|
|
} else if (this.mode === 3 && !this.selectedZones.empty) {
|
|
this.vacuumStartPreselectedZonesCleanup(debug);
|
|
}
|
|
}
|
|
|
|
drawCanvas() {
|
|
const canvas = this.getCanvas();
|
|
const context = canvas.getContext("2d");
|
|
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
context.translate(0.5, 0.5);
|
|
if (this._config.debug) {
|
|
let calibration_points = this._config.calibration_points;
|
|
if (this._config.camera_calibration) {
|
|
calibration_points = this._hass.states[this._config.map_camera].attributes.calibration_points;
|
|
}
|
|
for (const calibration_point of calibration_points) {
|
|
const {x, y} = this.convertVacuumToMapCoordinates(calibration_point.vacuum.x, calibration_point.vacuum.y);
|
|
this.drawCircle(context, x, y, 4, 'red', 1);
|
|
}
|
|
}
|
|
if (this.mode === 1 && this.currPoint.x != null) {
|
|
this.drawCircle(context, this.currPoint.x, this.currPoint.y, 4, 'yellow', 1);
|
|
} else if (this.mode === 2) {
|
|
for (let i = 0; i < this.rectangles.length; i++) {
|
|
const rect = this.rectangles[i];
|
|
context.beginPath();
|
|
if (i === this.selectedRectangle) {
|
|
context.setLineDash([10, 5]);
|
|
context.strokeStyle = 'white';
|
|
} else {
|
|
context.setLineDash([]);
|
|
context.strokeStyle = 'white';
|
|
context.fillStyle = 'rgba(255, 255, 255, 0.25)';
|
|
context.fillRect(rect.x, rect.y, rect.w, rect.h);
|
|
}
|
|
context.rect(rect.x, rect.y, rect.w, rect.h);
|
|
context.lineWidth = 1;
|
|
context.stroke();
|
|
this.drawDelete(context, rect.x + rect.w, rect.y);
|
|
this.drawResize(context, rect.x + rect.w, rect.y + rect.h);
|
|
}
|
|
if (this.isMouseDown && this.selectedRectangle < 0) {
|
|
context.beginPath();
|
|
context.setLineDash([10, 5]);
|
|
context.strokeStyle = 'white';
|
|
context.lineWidth = 1;
|
|
context.rect(this.currRectangle.x, this.currRectangle.y, this.currRectangle.w, this.currRectangle.h);
|
|
context.stroke();
|
|
}
|
|
} else if (this.mode === 3) {
|
|
for (let i = 0; i < this._config.zones.length; i++) {
|
|
const zone = this._config.zones[i];
|
|
for (const rect of zone) {
|
|
const {x, y, w, h} = this.convertVacuumToMapZone(rect[0], rect[1], rect[2], rect[3]);
|
|
context.beginPath();
|
|
context.setLineDash([]);
|
|
if (!this.selectedZones.includes(i)) {
|
|
context.strokeStyle = 'red';
|
|
context.fillStyle = 'rgba(255, 0, 0, 0.25)';
|
|
} else {
|
|
context.strokeStyle = 'green';
|
|
context.fillStyle = 'rgba(0, 255, 0, 0.25)';
|
|
}
|
|
context.lineWidth = 1;
|
|
context.rect(x, y, w, h);
|
|
context.fillRect(x, y, w, h);
|
|
context.stroke();
|
|
}
|
|
}
|
|
}
|
|
context.translate(-0.5, -0.5);
|
|
}
|
|
|
|
drawCircle(context, x, y, r, style, lineWidth) {
|
|
context.beginPath();
|
|
context.arc(x, y, r, 0, Math.PI * 2);
|
|
context.strokeStyle = style;
|
|
context.lineWidth = lineWidth;
|
|
context.stroke();
|
|
}
|
|
|
|
drawDelete(context, x, y) {
|
|
context.setLineDash([]);
|
|
this.drawCircle(context, x, y, 8, 'black', 1.2);
|
|
const diff = 4;
|
|
context.moveTo(x - diff, y - diff);
|
|
context.lineTo(x + diff, y + diff);
|
|
context.moveTo(x - diff, y + diff);
|
|
context.lineTo(x + diff, y - diff);
|
|
context.stroke();
|
|
}
|
|
|
|
drawResize(context, x, y) {
|
|
context.setLineDash([]);
|
|
this.drawCircle(context, x, y, 8, 'black', 1.2);
|
|
const diff = 4;
|
|
context.moveTo(x - diff, y - diff);
|
|
context.lineTo(x + diff, y + diff);
|
|
context.lineTo(x + diff, y + diff - 4);
|
|
context.lineTo(x + diff - 4, y + diff);
|
|
context.lineTo(x + diff, y + diff);
|
|
context.moveTo(x - diff, y - diff);
|
|
context.lineTo(x - diff, y - diff + 4);
|
|
context.lineTo(x - diff + 4, y - diff);
|
|
context.lineTo(x - diff, y - diff);
|
|
context.stroke();
|
|
}
|
|
|
|
getSelectedRectangle(x, y) {
|
|
let selected = -1;
|
|
let shouldDelete = false;
|
|
let shouldResize = false;
|
|
for (let i = this.rectangles.length - 1; i >= 0; i--) {
|
|
const rect = this.rectangles[i];
|
|
if (Math.pow(x - rect.x - rect.w, 2) + Math.pow(y - rect.y, 2) <= 64) {
|
|
selected = i;
|
|
shouldDelete = true;
|
|
break;
|
|
}
|
|
if (Math.pow(x - rect.x - rect.w, 2) + Math.pow(y - rect.y - rect.h, 2) <= 64) {
|
|
selected = i;
|
|
shouldResize = true;
|
|
break;
|
|
}
|
|
if (x >= rect.x && y >= rect.y && x <= rect.x + rect.w && y <= rect.y + rect.h) {
|
|
selected = i;
|
|
break;
|
|
}
|
|
}
|
|
return {selected, shouldDelete, shouldResize};
|
|
}
|
|
|
|
getSelectedZone(mx, my) {
|
|
let selected = -1;
|
|
for (let i = 0; i < this._config.zones.length && selected === -1; i++) {
|
|
const zone = this._config.zones[i];
|
|
for (const rect of zone) {
|
|
const {x, y, w, h} = this.convertVacuumToMapZone(rect[0], rect[1], rect[2], rect[3]);
|
|
if (mx >= x && my >= y && mx <= x + w && my <= y + h) {
|
|
selected = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return selected;
|
|
}
|
|
|
|
getCanvasStyle() {
|
|
if (this.mode === 2) return html`touch-action: none;`;
|
|
else return html``;
|
|
}
|
|
|
|
vacuumGoToPoint(debug) {
|
|
const mapPos = this.convertMapToVacuumCoordinates(this.currPoint.x, this.currPoint.y);
|
|
if (debug && this._config.debug) {
|
|
alert(JSON.stringify([mapPos.x, mapPos.y]));
|
|
} else {
|
|
this._hass.callService(this.service_domain, this.service_method, {
|
|
entity_id: this._config.entity,
|
|
command: "app_goto_target",
|
|
params: [mapPos.x, mapPos.y]
|
|
}).then(() => this.showToast());
|
|
}
|
|
}
|
|
|
|
vacuumStartZonedCleanup(debug) {
|
|
const zone = [];
|
|
for (const rect of this.rectangles) {
|
|
zone.push(this.convertMapToVacuumRect(rect, this.vacuumZonedCleanupRepeats));
|
|
}
|
|
if (debug && this._config.debug) {
|
|
alert(JSON.stringify(zone));
|
|
} else {
|
|
this._hass.callService(this.service_domain, this.service_method, {
|
|
entity_id: this._config.entity,
|
|
command: "app_zoned_clean",
|
|
params: zone
|
|
}).then(() => this.showToast());
|
|
}
|
|
}
|
|
|
|
vacuumStartPreselectedZonesCleanup(debug) {
|
|
const zone = [];
|
|
for (let i = 0; i < this.selectedZones.length; i++) {
|
|
const selectedZone = this.selectedZones[i];
|
|
const preselectedZone = this._config.zones[selectedZone];
|
|
for (const rect of preselectedZone) {
|
|
zone.push([rect[0], rect[1], rect[2], rect[3], this.vacuumZonedCleanupRepeats])
|
|
}
|
|
}
|
|
if (debug && this._config.debug) {
|
|
alert(JSON.stringify(zone));
|
|
} else {
|
|
this._hass.callService(this.service_domain, this.service_method, {
|
|
entity_id: this._config.entity,
|
|
command: "app_zoned_clean",
|
|
params: zone
|
|
}).then(() => this.showToast());
|
|
}
|
|
}
|
|
|
|
getCardSize() {
|
|
return 5;
|
|
}
|
|
|
|
convertMapToVacuumRect(rect, repeats) {
|
|
const xy1 = this.convertMapToVacuumCoordinates(rect.x, rect.y);
|
|
const xy2 = this.convertMapToVacuumCoordinates(rect.x + rect.w, rect.y + rect.h);
|
|
const x1 = Math.min(xy1.x, xy2.x);
|
|
const y1 = Math.min(xy1.y, xy2.y);
|
|
const x2 = Math.max(xy1.x, xy2.x);
|
|
const y2 = Math.max(xy1.y, xy2.y);
|
|
return [x1, y1, x2, y2, repeats];
|
|
}
|
|
|
|
convertMapToVacuumCoordinates(mapX, mapY) {
|
|
const {x, y} = this.coordinatesConverter.convertAB(mapX / this.imageScale, mapY / this.imageScale);
|
|
return {x: Math.round(x), y: Math.round(y)};
|
|
}
|
|
|
|
convertVacuumToMapZone(vacuumX1, vacuumY1, vacuumX2, vacuumY2) {
|
|
const {x: x1, y: y1} = this.convertVacuumToMapCoordinates(vacuumX1, vacuumY1);
|
|
const {x: x2, y: y2} = this.convertVacuumToMapCoordinates(vacuumX2, vacuumY2);
|
|
let x = Math.min(x1, x2);
|
|
let y = Math.min(y1, y2);
|
|
let w = Math.abs(x2 - x1);
|
|
let h = Math.abs(y2 - y1);
|
|
return {x, y, w, h};
|
|
}
|
|
|
|
convertVacuumToMapCoordinates(vacuumX, vacuumY) {
|
|
const {x: vX, y: vY} = this.coordinatesConverter.convertBA(vacuumX, vacuumY);
|
|
const x = Math.round(vX * this.imageScale);
|
|
const y = Math.round(vY * this.imageScale);
|
|
return {x, y};
|
|
}
|
|
|
|
getMapImage() {
|
|
return this.shadowRoot.getElementById("mapBackground");
|
|
}
|
|
|
|
getCanvas() {
|
|
return this.shadowRoot.getElementById("mapDrawing");
|
|
}
|
|
|
|
getPredefinedZonesIncreaseButton() {
|
|
return this.shadowRoot.getElementById("increaseButton");
|
|
}
|
|
|
|
getMousePos(evt) {
|
|
const canvas = this.getCanvas();
|
|
const rect = canvas.getBoundingClientRect();
|
|
return {
|
|
x: Math.round(evt.clientX - rect.left),
|
|
y: Math.round(evt.clientY - rect.top)
|
|
};
|
|
}
|
|
|
|
convertTouchToMouse(evt) {
|
|
if (evt.cancelable && this.mode === 2) {
|
|
evt.preventDefault();
|
|
}
|
|
return {
|
|
clientX: evt.changedTouches[0].clientX,
|
|
clientY: evt.changedTouches[0].clientY,
|
|
currentTarget: evt.currentTarget
|
|
};
|
|
}
|
|
|
|
showToast() {
|
|
const x = this.shadowRoot.getElementById("toast");
|
|
x.className = "show";
|
|
setTimeout(function () {
|
|
x.className = x.className.replace("show", "");
|
|
}, 2000);
|
|
}
|
|
|
|
updateCameraImage() {
|
|
this._hass.callWS({
|
|
type: 'camera_thumbnail',
|
|
entity_id: this._config.map_camera,
|
|
}).then(val => {
|
|
const {content_type: contentType, content} = val;
|
|
this.map_image = `data:${contentType};base64, ${content}`;
|
|
if (this._config.camera_calibration) {
|
|
if (!this._hass.states[this._config.map_camera].attributes.calibration_points) {
|
|
this.missingCameraAttribute = true;
|
|
} else {
|
|
this.updateCoordinates(this._hass.states[this._config.map_camera].attributes)
|
|
}
|
|
}
|
|
this.requestUpdate();
|
|
})
|
|
}
|
|
|
|
connectedCallback() {
|
|
super.connectedCallback();
|
|
if (this._config.map_camera) {
|
|
this.thumbUpdater = setInterval(() => this.updateCameraImage(), this._map_refresh_interval);
|
|
}
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
super.disconnectedCallback();
|
|
if (this._config.map_camera) {
|
|
clearInterval(this.thumbUpdater);
|
|
this.map_image = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
customElements.define('xiaomi-vacuum-map-card', XiaomiVacuumMapCard);
|