NEWS
Roborock S5 "cleaning area" per Tastendruck?
-
@Brati sagte in Roborock S5 "cleaning area" per Tastendruck?:
Einstellungen/ Staubsaugereinstellungen/ Kartenspeichermodus on/off
Ich habe aber die russische App, sollte in der Chinaversion jedoch genauso sein. Das geht erst, wenn einmal die Karte aufgebaut wurde. Danach bleibt sie immer so erhalten.
Meinst du MiHome App oder Flole VAC?
Habe bei mir Valetudo drauf, geht das dann trotzdem? -
@0018 Wenn du Valetudo hast, is die Karte egal. Mein Skript oben erkennt die Orientierung und dreht die Koordinaten entsprechend.
@AlCalzone Mal angenommen ich habe den Sauger durch meine Wohnung geschickt und habe dann eine komplette Karte aus der ich die Koordinaten für die Bereichsreinigung ziehen kann. Was ist denn wenn ich den Sauger dann mal per Knopfdruck am Gerät starten will? Dann generiert er doch eine neue Karte und die Bereichkoordinaten sind hinüber oder? Kann man das vermeiden?
-
@AlCalzone Mal angenommen ich habe den Sauger durch meine Wohnung geschickt und habe dann eine komplette Karte aus der ich die Koordinaten für die Bereichsreinigung ziehen kann. Was ist denn wenn ich den Sauger dann mal per Knopfdruck am Gerät starten will? Dann generiert er doch eine neue Karte und die Bereichkoordinaten sind hinüber oder? Kann man das vermeiden?
-
@0018 Nein, die Koordinaten stimmen immer noch.
Die Karte dreht sich beim Sauger der 1. Generation gerne, aber mein Skript erkennt dank Valetudo automatisch die Rotation und rechnet die Koordinaten entsprechend um.
@AlCalzone Ist es egal wie die Karte steht, wenn ich meine Koordinaten für die Raumzuweisung definiere?
-
@AlCalzone Ist es egal wie die Karte steht, wenn ich meine Koordinaten für die Raumzuweisung definiere?
@0018
Siehe Skript:// Koordiaten zählen von unten links (0,0) nach oben rechts (51200,51200) // Die folgenden Koordinaten gehen von nicht rotierter Karte (Winkel 0) ausWinkel 0 heißt "Robo links vom Dock". Wenn das bei dir anders ist, müsstest du in Zeile 119ff die Interpretation der Winkel ändern. Beispiel: Sauger steht bei dir normalerweise rechts, dann 180 => 0, 270 => 90, 90 => 270, 0 => 180.
-
@AlCalzone Vielen Dank.
Ich probiere gerade noch was anderes:
Alexa reagiert und spricht den Text.
Egal wie ich die Koordinaten eingebe. Der S50 macht keinen Mucks.
GFebe ich nur die ersten beiden Punkte ohne Klammern aber mit Komma an fährt er aus der Ladestation, irrt etwas umher und sagt dann er kann nicht zum Ziel fahren.Man müsste doch in den GoToPoint Datenpunkt direkt die Koordinate reinschreiben können?!
Habe alles probiert. Mit Klammern, ohne ... mit Kommas
Gebe ich ihm nur die ersten beiden Zahlen fährt er aus der Docking Station irrt kurz umher und sagt dann er kann nicht an die Koordinate fahren.
Im Log finde ich immer nur: GoTo only work with two arguments seperated by
LG Martin

@kaiserm Hey! Vielen Dank für das Blocky! Finde ich eine tolle Funktion! Habe ich gestern auch mal direkt nachgebaut, allerdings funktioniert es bei mir leider nicht. :-( Jetzt hoffe ich hier auf etwas Hilfe ...
Im Protokoll kommt die Meldung:09:35:03.244 info javascript.0 (1571) Start javascript script.js.common.Leere_den_Staubbehälter 09:35:03.247 info javascript.0 (1571) script.js.common.Leere_den_Staubbehälter: registered 1 subscription and 0 schedulesDer Roborock fährt weder los noch gibt die Alexa den Speak-Befehl aus. Allerdings gibt Sie als Antwort auf meine Spracheingabe die Bestätigung "ok" bzw. den "Pieps" aus.
Das Blocky scheint mir identisch nachgebaut zu sein. Aus folgenden Pfaden kommen die Variabeln:
-
summary: Alexa2/0/History/summary
-
speak: Alexa2/0/Echo-Devices/Geräte-NUMMER/Commands/speak
-
go to Point: Mihome-vacuum/0/control/goto
-
Die Koordianten kommen aus Flolevac (Vgl. @AlCalzone )
Screenshot Blocky mit Protokoll:

Oder muss ich noch etwas ganz anderes machen, bzw. haben damit das Script überhaupt funktioniert? Bin leider ein Noob, daher komme ich leider nicht weiter ...
Was ich auch noch probiert habe: Ich habe dem Script einen "smartName" gegeben, damit dieses im Cloud-Adapter als "Smart Gerät" auftaucht und dann in den Alexa-Routinen per Sprachsteuerung integrierbar wird. Auch um synonyme Befehle definieren zu können. Ist das richtig, bzw. notwendig mit dem Blocky-Script? Immerhin ist doch der speak-Befehl eine Bedingung im Script ...
Könnte mir bitte jemand helfen?
-
-
@kaiserm Also wie versprochen:
Hier die Variante, die bei mir im Einsatz ist (inklusive Test der Kartenorientierung via Valetudo). Muss als TypeScript angelegt werden und benötigt das zusätzliche Paket "axios":
import axios from "axios"; // Hier den Hostnamen und Zugangsdaten eintragen, unter dem Valetudo erreichbar ist const roboHostname = "rockrobo.fritz.box"; const valetudoAuth = { username: 'valetudo-username', password: 'valetudo-password' } // Hier den State eintragen, der den aktuellen Sauger-Status angibt const idVacuumState = "mihome-vacuum.0.info.state"; const center: Point = [25600, 25600]; // Koordiaten zählen von unten links (0,0) nach oben rechts (51200,51200) // Die folgenden Koordinaten gehen von nicht rotierter Karte (Winkel 0) aus const rooms: Record<string, Rectangle[]> = { "Küche": [[21600, 29100, 23500, 33100]], "Flur": [[18400, 24000, 22200, 27100], [19500, 27100, 21600, 30400]], "Wohnzimmer": [[17600, 20000, 21600, 24000], [21600, 19200, 24200, 23200]], "Schlafzimmer": [[24100, 23200, 26100, 27400], [22100, 23800, 24100, 27400]], "Bad": [[21500, 27500, 23200, 29000]], }; // =============================================================================== type Rectangle = [number, number, number, number]; type Point = [number, number]; /** rotates a rectangle by 90° around the absolute origin */ function rotate90([x1, y1, x2, y2]: Rectangle): Rectangle { return [-y2, x1, -y1, x2]; } /** rotates a rectangle by 180° around the absolute origin */ function rotate180([x1, y1, x2, y2]: Rectangle): Rectangle { return [-x2, -y2, -x1, -y1]; } /** Rotates a rectangle around the given center by the given angle */ function rotate(rect: Rectangle, center: Point, angle: number) { rect = [rect[0] - center[0], rect[1] - center[1], rect[2] - center[0], rect[3] - center[1]]; if (angle % 180 === 90) { rect = rotate90(rect); angle -= 90; } if (angle === 180) { rect = rotate180(rect); } rect = [rect[0] + center[0], rect[1] + center[1], rect[2] + center[0], rect[3] + center[1]]; return rect; } for (const room of Object.keys(rooms) as (keyof typeof rooms)[]) { createState(`Staubsauger.${room}`, { type: "boolean", read: true, write: true, role: "switch", name: `${room} saugen`, }); on({ id: `javascript.${instance}.Staubsauger.${room}`, val: true, ack: false }, async () => { if (getState(idVacuumState).val !== 8) await cancelCurrentAction(); if (getMapRotation() === -1 /* unbekannt */) { // We need to test the map rotation const rotation = await testMapRotation(); log(`Die Karte ist ${rotation !== 0 ? `um ${rotation}° ` : "nicht "}rotiert.`); await rememberMapRotation(rotation); } // Now that we know how the map is rotated, we can clean the room cleanRoom(room); }); } createState(`Staubsauger.stop`, { type: "boolean", read: true, write: true, role: "switch", name: `Staubsauger anhalten`, }); on({ id: `javascript.${instance}.Staubsauger.stop`, val: true, ack: false }, () => { stopCleanup(); }); const idMapRotated = "Staubsauger.info.mapRotated"; createState(idMapRotated, { type: "number", read: true, write: false, role: "indicator", states: { "-1": "unknown", "0": "Robo links vom Dock", "90": "Robo unten vom Dock", "180": "Robo rechts vom Dock", "270": "Robo oben vom Dock", }, name: `Wie die Karte rotiert ist`, }); /** * Bestimmt die Kartenrotation wenn der Staubsauger neben dem Dock steht * 0 Grad bedeutet, der Sauger steht links */ async function testMapRotation(): Promise<number> { log("Teste Kartenorientierung..."); const { data: { charger, robot } } = await axios({ url: `http://${roboHostname}/api/map/latest`, auth: valetudoAuth }); // Valetudo zählt von oben links nach unten rechts, d.h. die Y-Koordinaten // sind entgegengesetzt der mathematischen Definition let c2r = [robot[0] - charger[0], charger[1] - robot[1]]; const angle = Math.atan2(c2r[1], c2r[0]) * 180 / Math.PI; if (angle <= 45 && angle >= -45) { // Sauger steht rechts return 180; } else if (angle > 45 && angle < 135) { // Sauger steht oben return 270; } else if (angle < -45 && angle > -135) { // Sauger steht unten return 90; } else { return 0; } } function rememberMapRotation(rotation: number): Promise<void> { return setStateAsync(idMapRotated, rotation); } function getMapRotation(): number { return getState(idMapRotated).val; } // "Forget" map rotation when the vacuum starts charging on({ id: idVacuumState, val: 8 /* charging */ }, (obj) => { setState(idMapRotated, -1 /* unknown */); // And reset all control states for (const room of Object.keys(rooms) as (keyof typeof rooms)[]) { setState(`Staubsauger.${room}`, false, true); } setState(`Staubsauger.stop`, false, true); }); async function beginCleanup(): Promise<void> { } async function cancelCurrentAction(): Promise<void> { setState("mihome-vacuum.0.control.pause", true); // wait for the "paused" status before going home await waitForPauseOrSleep(); } async function stopCleanup(): Promise<void> { log(`Saugvorgang abgebrochen!`); setState(`Staubsauger.stop`, true, true); if (getState(idVacuumState).val !== 8) { await cancelCurrentAction(); await setStateAsync('mihome-vacuum.0.control.home', true); // wait for the "charging" status before resolving await waitFor(idVacuumState, 8); } log(`Staubsauger ist in der Basis`); setState(`Staubsauger.stop`, false, true); } async function cleanRoom(room: keyof typeof rooms): Promise<void> { log(`Saugvorgang für ${room} gestartet!`); setState(`Staubsauger.${room}`, true, true); const mapRotation = getMapRotation(); const originalCoords = rooms[room]; log(`original coordinates: ${JSON.stringify(originalCoords)}`); const roomCoords = rooms[room].map(rect => rotate(rect, center, mapRotation)); log(`rotated coordinates: ${JSON.stringify(roomCoords)}`); const coords = roomCoords[0]; const targetCoords = [ ((coords[0] + coords[2]) / 2).toFixed(0), ((coords[1] + coords[3]) / 2).toFixed(0), ] // go to center of first zone const gotoString = targetCoords.join(","); await setStateAsync("mihome-vacuum.0.control.goTo", gotoString); log(`Fahre zur Mitte von ${room}`); await wait(10000); await waitForPauseOrSleep(); if (getState("Staubsauger.stop").val) return; const zoneCleanString = roomCoords.map(zone => { return "[" + zone.concat(1).map(coord => coord.toString()).join(",") + "]"; }).join(","); log("Starte Zonenreinigung..."); await setStateAsync("mihome-vacuum.0.control.zoneClean", zoneCleanString); // wait for the cleanup to finish await waitFor(idVacuumState, 6); } async function waitForPauseOrSleep(): Promise<void> { log("Warte auf Zustand schlafen oder Pause..."); switch (getState(idVacuumState).val) { case 3: case 10: log(" => Zustand bereits aktiv!"); return; default: await Promise.race([ waitFor(idVacuumState, 10), waitFor(idVacuumState, 3), ]); log(" => Zustand erreicht!"); } } function wait(ms: number): Promise<void> { return new Promise(resolve => { setTimeout(resolve, ms); }); } function waitFor(stateID: string, value: any): Promise<void> { return new Promise(resolve => { const handler = (obj: iobJS.ChangedStateObject) => { if (obj.newState.val === value) { unsubscribe(handler); resolve(); } } subscribe(stateID, handler); }) } function setStateAsync(id: string, state: any): Promise<void> { return new Promise(res => { setState(id, state, () => res()); }); }Wenn du die automatische Rotation nicht benötigst oder kein Valetudo hast, wäre folgendes die vereinfachte Variante:
// Hier den State eintragen, der den aktuellen Sauger-Status angibt const idVacuumState = "mihome-vacuum.0.info.state"; // Koordiaten zählen von unten links (0,0) nach oben rechts (51200,51200) // Die folgenden Koordinaten gehen von nicht rotierter Karte (Winkel 0) aus const rooms: Record<string, Rectangle[]> = { "Küche": [[21600, 29100, 23500, 33100]], "Flur": [[18400, 24000, 22200, 27100], [19500, 27100, 21600, 30400]], "Wohnzimmer": [[17600, 20000, 21600, 24000], [21600, 19200, 24200, 23200]], "Schlafzimmer": [[24100, 23200, 26100, 27400], [22100, 23800, 24100, 27400]], "Bad": [[21500, 27500, 23200, 29000]], }; // =============================================================================== type Rectangle = [number, number, number, number]; for (const room of Object.keys(rooms) as (keyof typeof rooms)[]) { createState(`Staubsauger.${room}`, { type: "boolean", read: true, write: true, role: "switch", name: `${room} saugen`, }); on({ id: `javascript.${instance}.Staubsauger.${room}`, val: true, ack: false }, async () => { if (getState(idVacuumState).val !== 8) await cancelCurrentAction(); cleanRoom(room); }); } createState(`Staubsauger.stop`, { type: "boolean", read: true, write: true, role: "switch", name: `Staubsauger anhalten`, }); on({ id: `javascript.${instance}.Staubsauger.stop`, val: true, ack: false }, () => { stopCleanup(); }); // Reset all control states when the vacuum starts charging on({ id: idVacuumState, val: 8 /* charging */ }, (obj) => { for (const room of Object.keys(rooms) as (keyof typeof rooms)[]) { setState(`Staubsauger.${room}`, false, true); } setState(`Staubsauger.stop`, false, true); }); async function cancelCurrentAction(): Promise<void> { setState("mihome-vacuum.0.control.pause", true); // wait for the "paused" status before going home await waitForPauseOrSleep(); } async function stopCleanup(): Promise<void> { log(`Saugvorgang abgebrochen!`); setState(`Staubsauger.stop`, true, true); if (getState(idVacuumState).val !== 8) { await cancelCurrentAction(); await setStateAsync('mihome-vacuum.0.control.home', true); // wait for the "charging" status before resolving await waitFor(idVacuumState, 8); } log(`Staubsauger ist in der Basis`); setState(`Staubsauger.stop`, false, true); } async function cleanRoom(room: keyof typeof rooms): Promise<void> { log(`Saugvorgang für ${room} gestartet!`); setState(`Staubsauger.${room}`, true, true); const roomCoords = rooms[room]; const coords = roomCoords[0]; const targetCoords = [ ((coords[0] + coords[2]) / 2).toFixed(0), ((coords[1] + coords[3]) / 2).toFixed(0), ] // go to center of first zone const gotoString = targetCoords.join(","); await setStateAsync("mihome-vacuum.0.control.goTo", gotoString); log(`Fahre zur Mitte von ${room}`); await wait(10000); await waitForPauseOrSleep(); if (getState("Staubsauger.stop").val) return; const zoneCleanString = roomCoords.map(zone => { return "[" + zone.concat(1).map(coord => coord.toString()).join(",") + "]"; }).join(","); log("Starte Zonenreinigung..."); await setStateAsync("mihome-vacuum.0.control.zoneClean", zoneCleanString); // wait for the cleanup to finish await waitFor(idVacuumState, 6); } async function waitForPauseOrSleep(): Promise<void> { log("Warte auf Zustand schlafen oder Pause..."); switch (getState(idVacuumState).val) { case 3: case 10: log(" => Zustand bereits aktiv!"); return; default: await Promise.race([ waitFor(idVacuumState, 10), waitFor(idVacuumState, 3), ]); log(" => Zustand erreicht!"); } } function wait(ms: number): Promise<void> { return new Promise(resolve => { setTimeout(resolve, ms); }); } function waitFor(stateID: string, value: any): Promise<void> { return new Promise(resolve => { const handler = (obj: iobJS.ChangedStateObject) => { if (obj.newState.val === value) { unsubscribe(handler); resolve(); } } subscribe(stateID, handler); }) } function setStateAsync(id: string, state: any): Promise<void> { return new Promise(res => { setState(id, state, () => res()); }); }Beide Varianten legen für jeden Raum und für Stop einen State an, den du dann jeweils mit einer Alexa-Routine ansprechen kannst.
@AlCalzone Sauber! Das Script funktioniert nun auch bei mir hervorragend! Besten Dank! :blush: :clap:
-
@kaiserm Also wie versprochen:
Hier die Variante, die bei mir im Einsatz ist (inklusive Test der Kartenorientierung via Valetudo). Muss als TypeScript angelegt werden und benötigt das zusätzliche Paket "axios":
import axios from "axios"; // Hier den Hostnamen und Zugangsdaten eintragen, unter dem Valetudo erreichbar ist const roboHostname = "rockrobo.fritz.box"; const valetudoAuth = { username: 'valetudo-username', password: 'valetudo-password' } // Hier den State eintragen, der den aktuellen Sauger-Status angibt const idVacuumState = "mihome-vacuum.0.info.state"; const center: Point = [25600, 25600]; // Koordiaten zählen von unten links (0,0) nach oben rechts (51200,51200) // Die folgenden Koordinaten gehen von nicht rotierter Karte (Winkel 0) aus const rooms: Record<string, Rectangle[]> = { "Küche": [[21600, 29100, 23500, 33100]], "Flur": [[18400, 24000, 22200, 27100], [19500, 27100, 21600, 30400]], "Wohnzimmer": [[17600, 20000, 21600, 24000], [21600, 19200, 24200, 23200]], "Schlafzimmer": [[24100, 23200, 26100, 27400], [22100, 23800, 24100, 27400]], "Bad": [[21500, 27500, 23200, 29000]], }; // =============================================================================== type Rectangle = [number, number, number, number]; type Point = [number, number]; /** rotates a rectangle by 90° around the absolute origin */ function rotate90([x1, y1, x2, y2]: Rectangle): Rectangle { return [-y2, x1, -y1, x2]; } /** rotates a rectangle by 180° around the absolute origin */ function rotate180([x1, y1, x2, y2]: Rectangle): Rectangle { return [-x2, -y2, -x1, -y1]; } /** Rotates a rectangle around the given center by the given angle */ function rotate(rect: Rectangle, center: Point, angle: number) { rect = [rect[0] - center[0], rect[1] - center[1], rect[2] - center[0], rect[3] - center[1]]; if (angle % 180 === 90) { rect = rotate90(rect); angle -= 90; } if (angle === 180) { rect = rotate180(rect); } rect = [rect[0] + center[0], rect[1] + center[1], rect[2] + center[0], rect[3] + center[1]]; return rect; } for (const room of Object.keys(rooms) as (keyof typeof rooms)[]) { createState(`Staubsauger.${room}`, { type: "boolean", read: true, write: true, role: "switch", name: `${room} saugen`, }); on({ id: `javascript.${instance}.Staubsauger.${room}`, val: true, ack: false }, async () => { if (getState(idVacuumState).val !== 8) await cancelCurrentAction(); if (getMapRotation() === -1 /* unbekannt */) { // We need to test the map rotation const rotation = await testMapRotation(); log(`Die Karte ist ${rotation !== 0 ? `um ${rotation}° ` : "nicht "}rotiert.`); await rememberMapRotation(rotation); } // Now that we know how the map is rotated, we can clean the room cleanRoom(room); }); } createState(`Staubsauger.stop`, { type: "boolean", read: true, write: true, role: "switch", name: `Staubsauger anhalten`, }); on({ id: `javascript.${instance}.Staubsauger.stop`, val: true, ack: false }, () => { stopCleanup(); }); const idMapRotated = "Staubsauger.info.mapRotated"; createState(idMapRotated, { type: "number", read: true, write: false, role: "indicator", states: { "-1": "unknown", "0": "Robo links vom Dock", "90": "Robo unten vom Dock", "180": "Robo rechts vom Dock", "270": "Robo oben vom Dock", }, name: `Wie die Karte rotiert ist`, }); /** * Bestimmt die Kartenrotation wenn der Staubsauger neben dem Dock steht * 0 Grad bedeutet, der Sauger steht links */ async function testMapRotation(): Promise<number> { log("Teste Kartenorientierung..."); const { data: { charger, robot } } = await axios({ url: `http://${roboHostname}/api/map/latest`, auth: valetudoAuth }); // Valetudo zählt von oben links nach unten rechts, d.h. die Y-Koordinaten // sind entgegengesetzt der mathematischen Definition let c2r = [robot[0] - charger[0], charger[1] - robot[1]]; const angle = Math.atan2(c2r[1], c2r[0]) * 180 / Math.PI; if (angle <= 45 && angle >= -45) { // Sauger steht rechts return 180; } else if (angle > 45 && angle < 135) { // Sauger steht oben return 270; } else if (angle < -45 && angle > -135) { // Sauger steht unten return 90; } else { return 0; } } function rememberMapRotation(rotation: number): Promise<void> { return setStateAsync(idMapRotated, rotation); } function getMapRotation(): number { return getState(idMapRotated).val; } // "Forget" map rotation when the vacuum starts charging on({ id: idVacuumState, val: 8 /* charging */ }, (obj) => { setState(idMapRotated, -1 /* unknown */); // And reset all control states for (const room of Object.keys(rooms) as (keyof typeof rooms)[]) { setState(`Staubsauger.${room}`, false, true); } setState(`Staubsauger.stop`, false, true); }); async function beginCleanup(): Promise<void> { } async function cancelCurrentAction(): Promise<void> { setState("mihome-vacuum.0.control.pause", true); // wait for the "paused" status before going home await waitForPauseOrSleep(); } async function stopCleanup(): Promise<void> { log(`Saugvorgang abgebrochen!`); setState(`Staubsauger.stop`, true, true); if (getState(idVacuumState).val !== 8) { await cancelCurrentAction(); await setStateAsync('mihome-vacuum.0.control.home', true); // wait for the "charging" status before resolving await waitFor(idVacuumState, 8); } log(`Staubsauger ist in der Basis`); setState(`Staubsauger.stop`, false, true); } async function cleanRoom(room: keyof typeof rooms): Promise<void> { log(`Saugvorgang für ${room} gestartet!`); setState(`Staubsauger.${room}`, true, true); const mapRotation = getMapRotation(); const originalCoords = rooms[room]; log(`original coordinates: ${JSON.stringify(originalCoords)}`); const roomCoords = rooms[room].map(rect => rotate(rect, center, mapRotation)); log(`rotated coordinates: ${JSON.stringify(roomCoords)}`); const coords = roomCoords[0]; const targetCoords = [ ((coords[0] + coords[2]) / 2).toFixed(0), ((coords[1] + coords[3]) / 2).toFixed(0), ] // go to center of first zone const gotoString = targetCoords.join(","); await setStateAsync("mihome-vacuum.0.control.goTo", gotoString); log(`Fahre zur Mitte von ${room}`); await wait(10000); await waitForPauseOrSleep(); if (getState("Staubsauger.stop").val) return; const zoneCleanString = roomCoords.map(zone => { return "[" + zone.concat(1).map(coord => coord.toString()).join(",") + "]"; }).join(","); log("Starte Zonenreinigung..."); await setStateAsync("mihome-vacuum.0.control.zoneClean", zoneCleanString); // wait for the cleanup to finish await waitFor(idVacuumState, 6); } async function waitForPauseOrSleep(): Promise<void> { log("Warte auf Zustand schlafen oder Pause..."); switch (getState(idVacuumState).val) { case 3: case 10: log(" => Zustand bereits aktiv!"); return; default: await Promise.race([ waitFor(idVacuumState, 10), waitFor(idVacuumState, 3), ]); log(" => Zustand erreicht!"); } } function wait(ms: number): Promise<void> { return new Promise(resolve => { setTimeout(resolve, ms); }); } function waitFor(stateID: string, value: any): Promise<void> { return new Promise(resolve => { const handler = (obj: iobJS.ChangedStateObject) => { if (obj.newState.val === value) { unsubscribe(handler); resolve(); } } subscribe(stateID, handler); }) } function setStateAsync(id: string, state: any): Promise<void> { return new Promise(res => { setState(id, state, () => res()); }); }Wenn du die automatische Rotation nicht benötigst oder kein Valetudo hast, wäre folgendes die vereinfachte Variante:
// Hier den State eintragen, der den aktuellen Sauger-Status angibt const idVacuumState = "mihome-vacuum.0.info.state"; // Koordiaten zählen von unten links (0,0) nach oben rechts (51200,51200) // Die folgenden Koordinaten gehen von nicht rotierter Karte (Winkel 0) aus const rooms: Record<string, Rectangle[]> = { "Küche": [[21600, 29100, 23500, 33100]], "Flur": [[18400, 24000, 22200, 27100], [19500, 27100, 21600, 30400]], "Wohnzimmer": [[17600, 20000, 21600, 24000], [21600, 19200, 24200, 23200]], "Schlafzimmer": [[24100, 23200, 26100, 27400], [22100, 23800, 24100, 27400]], "Bad": [[21500, 27500, 23200, 29000]], }; // =============================================================================== type Rectangle = [number, number, number, number]; for (const room of Object.keys(rooms) as (keyof typeof rooms)[]) { createState(`Staubsauger.${room}`, { type: "boolean", read: true, write: true, role: "switch", name: `${room} saugen`, }); on({ id: `javascript.${instance}.Staubsauger.${room}`, val: true, ack: false }, async () => { if (getState(idVacuumState).val !== 8) await cancelCurrentAction(); cleanRoom(room); }); } createState(`Staubsauger.stop`, { type: "boolean", read: true, write: true, role: "switch", name: `Staubsauger anhalten`, }); on({ id: `javascript.${instance}.Staubsauger.stop`, val: true, ack: false }, () => { stopCleanup(); }); // Reset all control states when the vacuum starts charging on({ id: idVacuumState, val: 8 /* charging */ }, (obj) => { for (const room of Object.keys(rooms) as (keyof typeof rooms)[]) { setState(`Staubsauger.${room}`, false, true); } setState(`Staubsauger.stop`, false, true); }); async function cancelCurrentAction(): Promise<void> { setState("mihome-vacuum.0.control.pause", true); // wait for the "paused" status before going home await waitForPauseOrSleep(); } async function stopCleanup(): Promise<void> { log(`Saugvorgang abgebrochen!`); setState(`Staubsauger.stop`, true, true); if (getState(idVacuumState).val !== 8) { await cancelCurrentAction(); await setStateAsync('mihome-vacuum.0.control.home', true); // wait for the "charging" status before resolving await waitFor(idVacuumState, 8); } log(`Staubsauger ist in der Basis`); setState(`Staubsauger.stop`, false, true); } async function cleanRoom(room: keyof typeof rooms): Promise<void> { log(`Saugvorgang für ${room} gestartet!`); setState(`Staubsauger.${room}`, true, true); const roomCoords = rooms[room]; const coords = roomCoords[0]; const targetCoords = [ ((coords[0] + coords[2]) / 2).toFixed(0), ((coords[1] + coords[3]) / 2).toFixed(0), ] // go to center of first zone const gotoString = targetCoords.join(","); await setStateAsync("mihome-vacuum.0.control.goTo", gotoString); log(`Fahre zur Mitte von ${room}`); await wait(10000); await waitForPauseOrSleep(); if (getState("Staubsauger.stop").val) return; const zoneCleanString = roomCoords.map(zone => { return "[" + zone.concat(1).map(coord => coord.toString()).join(",") + "]"; }).join(","); log("Starte Zonenreinigung..."); await setStateAsync("mihome-vacuum.0.control.zoneClean", zoneCleanString); // wait for the cleanup to finish await waitFor(idVacuumState, 6); } async function waitForPauseOrSleep(): Promise<void> { log("Warte auf Zustand schlafen oder Pause..."); switch (getState(idVacuumState).val) { case 3: case 10: log(" => Zustand bereits aktiv!"); return; default: await Promise.race([ waitFor(idVacuumState, 10), waitFor(idVacuumState, 3), ]); log(" => Zustand erreicht!"); } } function wait(ms: number): Promise<void> { return new Promise(resolve => { setTimeout(resolve, ms); }); } function waitFor(stateID: string, value: any): Promise<void> { return new Promise(resolve => { const handler = (obj: iobJS.ChangedStateObject) => { if (obj.newState.val === value) { unsubscribe(handler); resolve(); } } subscribe(stateID, handler); }) } function setStateAsync(id: string, state: any): Promise<void> { return new Promise(res => { setState(id, state, () => res()); }); }Beide Varianten legen für jeden Raum und für Stop einen State an, den du dann jeweils mit einer Alexa-Routine ansprechen kannst.
@AlCalzone sagte in Roborock S5 "cleaning area" per Tastendruck?:
@kaiserm Also wie versprochen:
Muss als TypeScript angelegt werden und benötigt das zusätzliche Paket "axios":
Bekomme bei mir folgende Meldung:
javascript.0 2019-12-07 00:24:41.947 warn ERROR: Cannot find module 'axios'. javascript.0 2019-12-07 00:24:41.947 warn ^ javascript.0 2019-12-07 00:24:41.947 warn import axios from "axios"; javascript.0 2019-12-07 00:24:41.947 warn (2094) script.js.common.4_MiRobot.MiRobot_Zone_neu: TypeScript compilation had errors:Habe im JS Instanz das Paket axios hinzugefügt, aber die Meldung kommt trotzdem noch. Muss ich das noch irgendwo anders eintragen?
Gibt es einen Grund warum der Sauger erst zu einem Spot fährt, bevor die Zonenreinigung startet?
-
@AlCalzone sagte in Roborock S5 "cleaning area" per Tastendruck?:
@kaiserm Also wie versprochen:
Muss als TypeScript angelegt werden und benötigt das zusätzliche Paket "axios":
Bekomme bei mir folgende Meldung:
javascript.0 2019-12-07 00:24:41.947 warn ERROR: Cannot find module 'axios'. javascript.0 2019-12-07 00:24:41.947 warn ^ javascript.0 2019-12-07 00:24:41.947 warn import axios from "axios"; javascript.0 2019-12-07 00:24:41.947 warn (2094) script.js.common.4_MiRobot.MiRobot_Zone_neu: TypeScript compilation had errors:Habe im JS Instanz das Paket axios hinzugefügt, aber die Meldung kommt trotzdem noch. Muss ich das noch irgendwo anders eintragen?
Gibt es einen Grund warum der Sauger erst zu einem Spot fährt, bevor die Zonenreinigung startet?
@0018 sagte in Roborock S5 "cleaning area" per Tastendruck?:
Habe im JS Instanz das Paket axios hinzugefügt, aber die Meldung kommt trotzdem noch. Muss ich das noch irgendwo anders eintragen?
Ich bin mir nicht sicher, ob der aktuelle Adapter damit schon released ist. Wenn es die Option "Activate syntax help for these npm modules:" gibt, trag da auch noch axios ein. Ansonsten folgende Zeile über den Import schreiben:
// @ts-ignoreGibt es einen Grund warum der Sauger erst zu einem Spot fährt, bevor die Zonenreinigung startet?
Ja, das ist leiser als wenn er im Reinigungs-Modus in die Zone fährt.
-
@kaiserm Also wie versprochen:
Hier die Variante, die bei mir im Einsatz ist (inklusive Test der Kartenorientierung via Valetudo). Muss als TypeScript angelegt werden und benötigt das zusätzliche Paket "axios":
import axios from "axios"; // Hier den Hostnamen und Zugangsdaten eintragen, unter dem Valetudo erreichbar ist const roboHostname = "rockrobo.fritz.box"; const valetudoAuth = { username: 'valetudo-username', password: 'valetudo-password' } // Hier den State eintragen, der den aktuellen Sauger-Status angibt const idVacuumState = "mihome-vacuum.0.info.state"; const center: Point = [25600, 25600]; // Koordiaten zählen von unten links (0,0) nach oben rechts (51200,51200) // Die folgenden Koordinaten gehen von nicht rotierter Karte (Winkel 0) aus const rooms: Record<string, Rectangle[]> = { "Küche": [[21600, 29100, 23500, 33100]], "Flur": [[18400, 24000, 22200, 27100], [19500, 27100, 21600, 30400]], "Wohnzimmer": [[17600, 20000, 21600, 24000], [21600, 19200, 24200, 23200]], "Schlafzimmer": [[24100, 23200, 26100, 27400], [22100, 23800, 24100, 27400]], "Bad": [[21500, 27500, 23200, 29000]], }; // =============================================================================== type Rectangle = [number, number, number, number]; type Point = [number, number]; /** rotates a rectangle by 90° around the absolute origin */ function rotate90([x1, y1, x2, y2]: Rectangle): Rectangle { return [-y2, x1, -y1, x2]; } /** rotates a rectangle by 180° around the absolute origin */ function rotate180([x1, y1, x2, y2]: Rectangle): Rectangle { return [-x2, -y2, -x1, -y1]; } /** Rotates a rectangle around the given center by the given angle */ function rotate(rect: Rectangle, center: Point, angle: number) { rect = [rect[0] - center[0], rect[1] - center[1], rect[2] - center[0], rect[3] - center[1]]; if (angle % 180 === 90) { rect = rotate90(rect); angle -= 90; } if (angle === 180) { rect = rotate180(rect); } rect = [rect[0] + center[0], rect[1] + center[1], rect[2] + center[0], rect[3] + center[1]]; return rect; } for (const room of Object.keys(rooms) as (keyof typeof rooms)[]) { createState(`Staubsauger.${room}`, { type: "boolean", read: true, write: true, role: "switch", name: `${room} saugen`, }); on({ id: `javascript.${instance}.Staubsauger.${room}`, val: true, ack: false }, async () => { if (getState(idVacuumState).val !== 8) await cancelCurrentAction(); if (getMapRotation() === -1 /* unbekannt */) { // We need to test the map rotation const rotation = await testMapRotation(); log(`Die Karte ist ${rotation !== 0 ? `um ${rotation}° ` : "nicht "}rotiert.`); await rememberMapRotation(rotation); } // Now that we know how the map is rotated, we can clean the room cleanRoom(room); }); } createState(`Staubsauger.stop`, { type: "boolean", read: true, write: true, role: "switch", name: `Staubsauger anhalten`, }); on({ id: `javascript.${instance}.Staubsauger.stop`, val: true, ack: false }, () => { stopCleanup(); }); const idMapRotated = "Staubsauger.info.mapRotated"; createState(idMapRotated, { type: "number", read: true, write: false, role: "indicator", states: { "-1": "unknown", "0": "Robo links vom Dock", "90": "Robo unten vom Dock", "180": "Robo rechts vom Dock", "270": "Robo oben vom Dock", }, name: `Wie die Karte rotiert ist`, }); /** * Bestimmt die Kartenrotation wenn der Staubsauger neben dem Dock steht * 0 Grad bedeutet, der Sauger steht links */ async function testMapRotation(): Promise<number> { log("Teste Kartenorientierung..."); const { data: { charger, robot } } = await axios({ url: `http://${roboHostname}/api/map/latest`, auth: valetudoAuth }); // Valetudo zählt von oben links nach unten rechts, d.h. die Y-Koordinaten // sind entgegengesetzt der mathematischen Definition let c2r = [robot[0] - charger[0], charger[1] - robot[1]]; const angle = Math.atan2(c2r[1], c2r[0]) * 180 / Math.PI; if (angle <= 45 && angle >= -45) { // Sauger steht rechts return 180; } else if (angle > 45 && angle < 135) { // Sauger steht oben return 270; } else if (angle < -45 && angle > -135) { // Sauger steht unten return 90; } else { return 0; } } function rememberMapRotation(rotation: number): Promise<void> { return setStateAsync(idMapRotated, rotation); } function getMapRotation(): number { return getState(idMapRotated).val; } // "Forget" map rotation when the vacuum starts charging on({ id: idVacuumState, val: 8 /* charging */ }, (obj) => { setState(idMapRotated, -1 /* unknown */); // And reset all control states for (const room of Object.keys(rooms) as (keyof typeof rooms)[]) { setState(`Staubsauger.${room}`, false, true); } setState(`Staubsauger.stop`, false, true); }); async function beginCleanup(): Promise<void> { } async function cancelCurrentAction(): Promise<void> { setState("mihome-vacuum.0.control.pause", true); // wait for the "paused" status before going home await waitForPauseOrSleep(); } async function stopCleanup(): Promise<void> { log(`Saugvorgang abgebrochen!`); setState(`Staubsauger.stop`, true, true); if (getState(idVacuumState).val !== 8) { await cancelCurrentAction(); await setStateAsync('mihome-vacuum.0.control.home', true); // wait for the "charging" status before resolving await waitFor(idVacuumState, 8); } log(`Staubsauger ist in der Basis`); setState(`Staubsauger.stop`, false, true); } async function cleanRoom(room: keyof typeof rooms): Promise<void> { log(`Saugvorgang für ${room} gestartet!`); setState(`Staubsauger.${room}`, true, true); const mapRotation = getMapRotation(); const originalCoords = rooms[room]; log(`original coordinates: ${JSON.stringify(originalCoords)}`); const roomCoords = rooms[room].map(rect => rotate(rect, center, mapRotation)); log(`rotated coordinates: ${JSON.stringify(roomCoords)}`); const coords = roomCoords[0]; const targetCoords = [ ((coords[0] + coords[2]) / 2).toFixed(0), ((coords[1] + coords[3]) / 2).toFixed(0), ] // go to center of first zone const gotoString = targetCoords.join(","); await setStateAsync("mihome-vacuum.0.control.goTo", gotoString); log(`Fahre zur Mitte von ${room}`); await wait(10000); await waitForPauseOrSleep(); if (getState("Staubsauger.stop").val) return; const zoneCleanString = roomCoords.map(zone => { return "[" + zone.concat(1).map(coord => coord.toString()).join(",") + "]"; }).join(","); log("Starte Zonenreinigung..."); await setStateAsync("mihome-vacuum.0.control.zoneClean", zoneCleanString); // wait for the cleanup to finish await waitFor(idVacuumState, 6); } async function waitForPauseOrSleep(): Promise<void> { log("Warte auf Zustand schlafen oder Pause..."); switch (getState(idVacuumState).val) { case 3: case 10: log(" => Zustand bereits aktiv!"); return; default: await Promise.race([ waitFor(idVacuumState, 10), waitFor(idVacuumState, 3), ]); log(" => Zustand erreicht!"); } } function wait(ms: number): Promise<void> { return new Promise(resolve => { setTimeout(resolve, ms); }); } function waitFor(stateID: string, value: any): Promise<void> { return new Promise(resolve => { const handler = (obj: iobJS.ChangedStateObject) => { if (obj.newState.val === value) { unsubscribe(handler); resolve(); } } subscribe(stateID, handler); }) } function setStateAsync(id: string, state: any): Promise<void> { return new Promise(res => { setState(id, state, () => res()); }); }Wenn du die automatische Rotation nicht benötigst oder kein Valetudo hast, wäre folgendes die vereinfachte Variante:
// Hier den State eintragen, der den aktuellen Sauger-Status angibt const idVacuumState = "mihome-vacuum.0.info.state"; // Koordiaten zählen von unten links (0,0) nach oben rechts (51200,51200) // Die folgenden Koordinaten gehen von nicht rotierter Karte (Winkel 0) aus const rooms: Record<string, Rectangle[]> = { "Küche": [[21600, 29100, 23500, 33100]], "Flur": [[18400, 24000, 22200, 27100], [19500, 27100, 21600, 30400]], "Wohnzimmer": [[17600, 20000, 21600, 24000], [21600, 19200, 24200, 23200]], "Schlafzimmer": [[24100, 23200, 26100, 27400], [22100, 23800, 24100, 27400]], "Bad": [[21500, 27500, 23200, 29000]], }; // =============================================================================== type Rectangle = [number, number, number, number]; for (const room of Object.keys(rooms) as (keyof typeof rooms)[]) { createState(`Staubsauger.${room}`, { type: "boolean", read: true, write: true, role: "switch", name: `${room} saugen`, }); on({ id: `javascript.${instance}.Staubsauger.${room}`, val: true, ack: false }, async () => { if (getState(idVacuumState).val !== 8) await cancelCurrentAction(); cleanRoom(room); }); } createState(`Staubsauger.stop`, { type: "boolean", read: true, write: true, role: "switch", name: `Staubsauger anhalten`, }); on({ id: `javascript.${instance}.Staubsauger.stop`, val: true, ack: false }, () => { stopCleanup(); }); // Reset all control states when the vacuum starts charging on({ id: idVacuumState, val: 8 /* charging */ }, (obj) => { for (const room of Object.keys(rooms) as (keyof typeof rooms)[]) { setState(`Staubsauger.${room}`, false, true); } setState(`Staubsauger.stop`, false, true); }); async function cancelCurrentAction(): Promise<void> { setState("mihome-vacuum.0.control.pause", true); // wait for the "paused" status before going home await waitForPauseOrSleep(); } async function stopCleanup(): Promise<void> { log(`Saugvorgang abgebrochen!`); setState(`Staubsauger.stop`, true, true); if (getState(idVacuumState).val !== 8) { await cancelCurrentAction(); await setStateAsync('mihome-vacuum.0.control.home', true); // wait for the "charging" status before resolving await waitFor(idVacuumState, 8); } log(`Staubsauger ist in der Basis`); setState(`Staubsauger.stop`, false, true); } async function cleanRoom(room: keyof typeof rooms): Promise<void> { log(`Saugvorgang für ${room} gestartet!`); setState(`Staubsauger.${room}`, true, true); const roomCoords = rooms[room]; const coords = roomCoords[0]; const targetCoords = [ ((coords[0] + coords[2]) / 2).toFixed(0), ((coords[1] + coords[3]) / 2).toFixed(0), ] // go to center of first zone const gotoString = targetCoords.join(","); await setStateAsync("mihome-vacuum.0.control.goTo", gotoString); log(`Fahre zur Mitte von ${room}`); await wait(10000); await waitForPauseOrSleep(); if (getState("Staubsauger.stop").val) return; const zoneCleanString = roomCoords.map(zone => { return "[" + zone.concat(1).map(coord => coord.toString()).join(",") + "]"; }).join(","); log("Starte Zonenreinigung..."); await setStateAsync("mihome-vacuum.0.control.zoneClean", zoneCleanString); // wait for the cleanup to finish await waitFor(idVacuumState, 6); } async function waitForPauseOrSleep(): Promise<void> { log("Warte auf Zustand schlafen oder Pause..."); switch (getState(idVacuumState).val) { case 3: case 10: log(" => Zustand bereits aktiv!"); return; default: await Promise.race([ waitFor(idVacuumState, 10), waitFor(idVacuumState, 3), ]); log(" => Zustand erreicht!"); } } function wait(ms: number): Promise<void> { return new Promise(resolve => { setTimeout(resolve, ms); }); } function waitFor(stateID: string, value: any): Promise<void> { return new Promise(resolve => { const handler = (obj: iobJS.ChangedStateObject) => { if (obj.newState.val === value) { unsubscribe(handler); resolve(); } } subscribe(stateID, handler); }) } function setStateAsync(id: string, state: any): Promise<void> { return new Promise(res => { setState(id, state, () => res()); }); }Beide Varianten legen für jeden Raum und für Stop einen State an, den du dann jeweils mit einer Alexa-Routine ansprechen kannst.
Hey @AlCalzone kann man dein Script denn auch so anpassen/erweitern, dass es zusätzlich zu den Räumen auch Positionen gibt, die nur angefahren werden könnten? Um z.B. den Staubbehälter vor dem Mülleimer leeren zu können? "Fahre Staubsauger zum Mülleimer"
Zudem wäre es denke ich auch interessant wenn man die Saugstufen in Abhängigkeit machen könnten. Im Bad könnte ich beispielsweise immer eine starke Stufe vertragen da hier unser Katzenklo steht. Oder kann man es in Abhängikeit vom Raum so einstellen, dass der ZoneCleanup auch mehrfach ausgeführt wird? Über ein paar Scripterweiterungen würde ich micht mega freuen :-) -
Hey @AlCalzone kann man dein Script denn auch so anpassen/erweitern, dass es zusätzlich zu den Räumen auch Positionen gibt, die nur angefahren werden könnten? Um z.B. den Staubbehälter vor dem Mülleimer leeren zu können? "Fahre Staubsauger zum Mülleimer"
Zudem wäre es denke ich auch interessant wenn man die Saugstufen in Abhängigkeit machen könnten. Im Bad könnte ich beispielsweise immer eine starke Stufe vertragen da hier unser Katzenklo steht. Oder kann man es in Abhängikeit vom Raum so einstellen, dass der ZoneCleanup auch mehrfach ausgeführt wird? Über ein paar Scripterweiterungen würde ich micht mega freuen :-) -
@AlCalzone Freut mich sehr :+1:
-
@0018
Siehe Skript:// Koordiaten zählen von unten links (0,0) nach oben rechts (51200,51200) // Die folgenden Koordinaten gehen von nicht rotierter Karte (Winkel 0) ausWinkel 0 heißt "Robo links vom Dock". Wenn das bei dir anders ist, müsstest du in Zeile 119ff die Interpretation der Winkel ändern. Beispiel: Sauger steht bei dir normalerweise rechts, dann 180 => 0, 270 => 90, 90 => 270, 0 => 180.
@AlCalzone sagte in Roborock S5 "cleaning area" per Tastendruck?:
Beispiel: Sauger steht bei dir normalerweise rechts, dann 180 => 0, 270 => 90, 90 => 270, 0 => 180.Ja genau, mein Sauger steht rechts vom Dock. Habe jetzt nachdem ich die Daten angepasst habe, leider das Problem das meine Bereiche nicht mehr mit den Koordinaten aus der Valetudo Map
übereinstimmen. Muss ich hier auch noch was drehen? Den die Zonenkordination sind ja dann auch gedreht oder? -
@AlCalzone sagte in Roborock S5 "cleaning area" per Tastendruck?:
Beispiel: Sauger steht bei dir normalerweise rechts, dann 180 => 0, 270 => 90, 90 => 270, 0 => 180.Ja genau, mein Sauger steht rechts vom Dock. Habe jetzt nachdem ich die Daten angepasst habe, leider das Problem das meine Bereiche nicht mehr mit den Koordinaten aus der Valetudo Map
übereinstimmen. Muss ich hier auch noch was drehen? Den die Zonenkordination sind ja dann auch gedreht oder? -
@AlCalzone Damit das ganze besser zu erklären ist hier mit Screenshots.
-
So sieht meine Karte in Valetudo und die dazugehörigen Koordinaten der Zone1 aus:

-
Aus meiner Sicht steht mein Robo rechts vom Dock, daher folgende Anpassung deines Scriptes:
const rooms: Record<string, Rectangle[]> = { "Test": [[26397, 22791, 29302, 24138]], };ab Zeile 119:
// Valetudo zählt von oben links nach unten rechts, d.h. die Y-Koordinaten // sind entgegengesetzt der mathematischen Definition let c2r = [robot[0] - charger[0], charger[1] - robot[1]]; const angle = Math.atan2(c2r[1], c2r[0]) * 180 / Math.PI; log("angle = " + angle); if (angle <= 45 && angle >= -45) { log("Sauger steht rechts"); return 0; } else if (angle > 45 && angle < 135) { log("Sauger steht oben"); return 90; } else if (angle < -45 && angle > -135) { log("Sauger steht unten"); return 270; } else { return 180; } }-
Wenn ich dann die Zonnenreinigung starte, sieht das auf der Vaeltuda Map dann wie folgt aus:

-
Als test mal mit dem Standard:
// Valetudo zählt von oben links nach unten rechts, d.h. die Y-Koordinaten // sind entgegengesetzt der mathematischen Definition let c2r = [robot[0] - charger[0], charger[1] - robot[1]]; const angle = Math.atan2(c2r[1], c2r[0]) * 180 / Math.PI; log("angle = " + angle); if (angle <= 45 && angle >= -45) { log("Sauger steht rechts"); return 180; } else if (angle > 45 && angle < 135) { log("Sauger steht oben"); return 270; } else if (angle < -45 && angle > -135) { log("Sauger steht unten"); return 90; } else { return 0; } }ergibt folgendes Ergebniss:

Habe auch schon 90 + 270 probiert, leider auch ohne Erfolg.
Im Script habe ich mal den Angle mit geloggt und bei den Versuchen kommt immer ähnliches raus....javascript.0 2019-12-09 21:12:16.299 info (2094) script.js.common.4_MiRobot.MiRobot_Zone_neu: angle = -2.981461219982192 javascript.0 2019-12-09 21:06:13.153 info (2094) script.js.common.4_MiRobot.MiRobot_Zone_neu: angle = 2.8912695962205643 javascript.0 2019-12-09 20:56:30.228 info (2094) script.js.common.4_MiRobot.MiRobot_Zone_neu: angle = -1.461310090881229Vielleicht siehst du ja auf Anhieb woran es liegt?
-
-
@AlCalzone Damit das ganze besser zu erklären ist hier mit Screenshots.
-
So sieht meine Karte in Valetudo und die dazugehörigen Koordinaten der Zone1 aus:

-
Aus meiner Sicht steht mein Robo rechts vom Dock, daher folgende Anpassung deines Scriptes:
const rooms: Record<string, Rectangle[]> = { "Test": [[26397, 22791, 29302, 24138]], };ab Zeile 119:
// Valetudo zählt von oben links nach unten rechts, d.h. die Y-Koordinaten // sind entgegengesetzt der mathematischen Definition let c2r = [robot[0] - charger[0], charger[1] - robot[1]]; const angle = Math.atan2(c2r[1], c2r[0]) * 180 / Math.PI; log("angle = " + angle); if (angle <= 45 && angle >= -45) { log("Sauger steht rechts"); return 0; } else if (angle > 45 && angle < 135) { log("Sauger steht oben"); return 90; } else if (angle < -45 && angle > -135) { log("Sauger steht unten"); return 270; } else { return 180; } }-
Wenn ich dann die Zonnenreinigung starte, sieht das auf der Vaeltuda Map dann wie folgt aus:

-
Als test mal mit dem Standard:
// Valetudo zählt von oben links nach unten rechts, d.h. die Y-Koordinaten // sind entgegengesetzt der mathematischen Definition let c2r = [robot[0] - charger[0], charger[1] - robot[1]]; const angle = Math.atan2(c2r[1], c2r[0]) * 180 / Math.PI; log("angle = " + angle); if (angle <= 45 && angle >= -45) { log("Sauger steht rechts"); return 180; } else if (angle > 45 && angle < 135) { log("Sauger steht oben"); return 270; } else if (angle < -45 && angle > -135) { log("Sauger steht unten"); return 90; } else { return 0; } }ergibt folgendes Ergebniss:

Habe auch schon 90 + 270 probiert, leider auch ohne Erfolg.
Im Script habe ich mal den Angle mit geloggt und bei den Versuchen kommt immer ähnliches raus....javascript.0 2019-12-09 21:12:16.299 info (2094) script.js.common.4_MiRobot.MiRobot_Zone_neu: angle = -2.981461219982192 javascript.0 2019-12-09 21:06:13.153 info (2094) script.js.common.4_MiRobot.MiRobot_Zone_neu: angle = 2.8912695962205643 javascript.0 2019-12-09 20:56:30.228 info (2094) script.js.common.4_MiRobot.MiRobot_Zone_neu: angle = -1.461310090881229Vielleicht siehst du ja auf Anhieb woran es liegt?
@0018 sagte in Roborock S5 "cleaning area" per Tastendruck?:
Vielleicht siehst du ja auf Anhieb woran es liegt?
Auf dem 1. Bild im Dock sieht es für mich so aus als zeigt der Sauger nach links, d.h. Winkel 0°. Hast du die Koordinaten direkt aus Valetudo? Dann müssen ggf. die Y-Koordinaten aller Zonen invertiert werden, da Valetudo anscheinend anders zählt als der Sauger selbst:
y(für Skript) = 51200 - y(Valetudo) -
-
@0018 sagte in Roborock S5 "cleaning area" per Tastendruck?:
Vielleicht siehst du ja auf Anhieb woran es liegt?
Auf dem 1. Bild im Dock sieht es für mich so aus als zeigt der Sauger nach links, d.h. Winkel 0°. Hast du die Koordinaten direkt aus Valetudo? Dann müssen ggf. die Y-Koordinaten aller Zonen invertiert werden, da Valetudo anscheinend anders zählt als der Sauger selbst:
y(für Skript) = 51200 - y(Valetudo)@AlCalzone Ja, die Koordinaten sind direkt aus Valetudo unter http://IPdesSaugers/zone
Habe jetzt die Y-Koordinaten nach Vorgabe umgerechnet und jetzt stimmen die Zonen überein.
Der Tipp mit// @ts-ignorehat auch funktioniert. Scheint also auf den ersten Blick erstmal alles zu passen. Ich werde es jetzt die nächsten Wochen mal testen.
Vielen Dank!
-
@kaiserm Also wie versprochen:
Hier die Variante, die bei mir im Einsatz ist (inklusive Test der Kartenorientierung via Valetudo). Muss als TypeScript angelegt werden und benötigt das zusätzliche Paket "axios":
import axios from "axios"; // Hier den Hostnamen und Zugangsdaten eintragen, unter dem Valetudo erreichbar ist const roboHostname = "rockrobo.fritz.box"; const valetudoAuth = { username: 'valetudo-username', password: 'valetudo-password' } // Hier den State eintragen, der den aktuellen Sauger-Status angibt const idVacuumState = "mihome-vacuum.0.info.state"; const center: Point = [25600, 25600]; // Koordiaten zählen von unten links (0,0) nach oben rechts (51200,51200) // Die folgenden Koordinaten gehen von nicht rotierter Karte (Winkel 0) aus const rooms: Record<string, Rectangle[]> = { "Küche": [[21600, 29100, 23500, 33100]], "Flur": [[18400, 24000, 22200, 27100], [19500, 27100, 21600, 30400]], "Wohnzimmer": [[17600, 20000, 21600, 24000], [21600, 19200, 24200, 23200]], "Schlafzimmer": [[24100, 23200, 26100, 27400], [22100, 23800, 24100, 27400]], "Bad": [[21500, 27500, 23200, 29000]], }; // =============================================================================== type Rectangle = [number, number, number, number]; type Point = [number, number]; /** rotates a rectangle by 90° around the absolute origin */ function rotate90([x1, y1, x2, y2]: Rectangle): Rectangle { return [-y2, x1, -y1, x2]; } /** rotates a rectangle by 180° around the absolute origin */ function rotate180([x1, y1, x2, y2]: Rectangle): Rectangle { return [-x2, -y2, -x1, -y1]; } /** Rotates a rectangle around the given center by the given angle */ function rotate(rect: Rectangle, center: Point, angle: number) { rect = [rect[0] - center[0], rect[1] - center[1], rect[2] - center[0], rect[3] - center[1]]; if (angle % 180 === 90) { rect = rotate90(rect); angle -= 90; } if (angle === 180) { rect = rotate180(rect); } rect = [rect[0] + center[0], rect[1] + center[1], rect[2] + center[0], rect[3] + center[1]]; return rect; } for (const room of Object.keys(rooms) as (keyof typeof rooms)[]) { createState(`Staubsauger.${room}`, { type: "boolean", read: true, write: true, role: "switch", name: `${room} saugen`, }); on({ id: `javascript.${instance}.Staubsauger.${room}`, val: true, ack: false }, async () => { if (getState(idVacuumState).val !== 8) await cancelCurrentAction(); if (getMapRotation() === -1 /* unbekannt */) { // We need to test the map rotation const rotation = await testMapRotation(); log(`Die Karte ist ${rotation !== 0 ? `um ${rotation}° ` : "nicht "}rotiert.`); await rememberMapRotation(rotation); } // Now that we know how the map is rotated, we can clean the room cleanRoom(room); }); } createState(`Staubsauger.stop`, { type: "boolean", read: true, write: true, role: "switch", name: `Staubsauger anhalten`, }); on({ id: `javascript.${instance}.Staubsauger.stop`, val: true, ack: false }, () => { stopCleanup(); }); const idMapRotated = "Staubsauger.info.mapRotated"; createState(idMapRotated, { type: "number", read: true, write: false, role: "indicator", states: { "-1": "unknown", "0": "Robo links vom Dock", "90": "Robo unten vom Dock", "180": "Robo rechts vom Dock", "270": "Robo oben vom Dock", }, name: `Wie die Karte rotiert ist`, }); /** * Bestimmt die Kartenrotation wenn der Staubsauger neben dem Dock steht * 0 Grad bedeutet, der Sauger steht links */ async function testMapRotation(): Promise<number> { log("Teste Kartenorientierung..."); const { data: { charger, robot } } = await axios({ url: `http://${roboHostname}/api/map/latest`, auth: valetudoAuth }); // Valetudo zählt von oben links nach unten rechts, d.h. die Y-Koordinaten // sind entgegengesetzt der mathematischen Definition let c2r = [robot[0] - charger[0], charger[1] - robot[1]]; const angle = Math.atan2(c2r[1], c2r[0]) * 180 / Math.PI; if (angle <= 45 && angle >= -45) { // Sauger steht rechts return 180; } else if (angle > 45 && angle < 135) { // Sauger steht oben return 270; } else if (angle < -45 && angle > -135) { // Sauger steht unten return 90; } else { return 0; } } function rememberMapRotation(rotation: number): Promise<void> { return setStateAsync(idMapRotated, rotation); } function getMapRotation(): number { return getState(idMapRotated).val; } // "Forget" map rotation when the vacuum starts charging on({ id: idVacuumState, val: 8 /* charging */ }, (obj) => { setState(idMapRotated, -1 /* unknown */); // And reset all control states for (const room of Object.keys(rooms) as (keyof typeof rooms)[]) { setState(`Staubsauger.${room}`, false, true); } setState(`Staubsauger.stop`, false, true); }); async function beginCleanup(): Promise<void> { } async function cancelCurrentAction(): Promise<void> { setState("mihome-vacuum.0.control.pause", true); // wait for the "paused" status before going home await waitForPauseOrSleep(); } async function stopCleanup(): Promise<void> { log(`Saugvorgang abgebrochen!`); setState(`Staubsauger.stop`, true, true); if (getState(idVacuumState).val !== 8) { await cancelCurrentAction(); await setStateAsync('mihome-vacuum.0.control.home', true); // wait for the "charging" status before resolving await waitFor(idVacuumState, 8); } log(`Staubsauger ist in der Basis`); setState(`Staubsauger.stop`, false, true); } async function cleanRoom(room: keyof typeof rooms): Promise<void> { log(`Saugvorgang für ${room} gestartet!`); setState(`Staubsauger.${room}`, true, true); const mapRotation = getMapRotation(); const originalCoords = rooms[room]; log(`original coordinates: ${JSON.stringify(originalCoords)}`); const roomCoords = rooms[room].map(rect => rotate(rect, center, mapRotation)); log(`rotated coordinates: ${JSON.stringify(roomCoords)}`); const coords = roomCoords[0]; const targetCoords = [ ((coords[0] + coords[2]) / 2).toFixed(0), ((coords[1] + coords[3]) / 2).toFixed(0), ] // go to center of first zone const gotoString = targetCoords.join(","); await setStateAsync("mihome-vacuum.0.control.goTo", gotoString); log(`Fahre zur Mitte von ${room}`); await wait(10000); await waitForPauseOrSleep(); if (getState("Staubsauger.stop").val) return; const zoneCleanString = roomCoords.map(zone => { return "[" + zone.concat(1).map(coord => coord.toString()).join(",") + "]"; }).join(","); log("Starte Zonenreinigung..."); await setStateAsync("mihome-vacuum.0.control.zoneClean", zoneCleanString); // wait for the cleanup to finish await waitFor(idVacuumState, 6); } async function waitForPauseOrSleep(): Promise<void> { log("Warte auf Zustand schlafen oder Pause..."); switch (getState(idVacuumState).val) { case 3: case 10: log(" => Zustand bereits aktiv!"); return; default: await Promise.race([ waitFor(idVacuumState, 10), waitFor(idVacuumState, 3), ]); log(" => Zustand erreicht!"); } } function wait(ms: number): Promise<void> { return new Promise(resolve => { setTimeout(resolve, ms); }); } function waitFor(stateID: string, value: any): Promise<void> { return new Promise(resolve => { const handler = (obj: iobJS.ChangedStateObject) => { if (obj.newState.val === value) { unsubscribe(handler); resolve(); } } subscribe(stateID, handler); }) } function setStateAsync(id: string, state: any): Promise<void> { return new Promise(res => { setState(id, state, () => res()); }); }Wenn du die automatische Rotation nicht benötigst oder kein Valetudo hast, wäre folgendes die vereinfachte Variante:
// Hier den State eintragen, der den aktuellen Sauger-Status angibt const idVacuumState = "mihome-vacuum.0.info.state"; // Koordiaten zählen von unten links (0,0) nach oben rechts (51200,51200) // Die folgenden Koordinaten gehen von nicht rotierter Karte (Winkel 0) aus const rooms: Record<string, Rectangle[]> = { "Küche": [[21600, 29100, 23500, 33100]], "Flur": [[18400, 24000, 22200, 27100], [19500, 27100, 21600, 30400]], "Wohnzimmer": [[17600, 20000, 21600, 24000], [21600, 19200, 24200, 23200]], "Schlafzimmer": [[24100, 23200, 26100, 27400], [22100, 23800, 24100, 27400]], "Bad": [[21500, 27500, 23200, 29000]], }; // =============================================================================== type Rectangle = [number, number, number, number]; for (const room of Object.keys(rooms) as (keyof typeof rooms)[]) { createState(`Staubsauger.${room}`, { type: "boolean", read: true, write: true, role: "switch", name: `${room} saugen`, }); on({ id: `javascript.${instance}.Staubsauger.${room}`, val: true, ack: false }, async () => { if (getState(idVacuumState).val !== 8) await cancelCurrentAction(); cleanRoom(room); }); } createState(`Staubsauger.stop`, { type: "boolean", read: true, write: true, role: "switch", name: `Staubsauger anhalten`, }); on({ id: `javascript.${instance}.Staubsauger.stop`, val: true, ack: false }, () => { stopCleanup(); }); // Reset all control states when the vacuum starts charging on({ id: idVacuumState, val: 8 /* charging */ }, (obj) => { for (const room of Object.keys(rooms) as (keyof typeof rooms)[]) { setState(`Staubsauger.${room}`, false, true); } setState(`Staubsauger.stop`, false, true); }); async function cancelCurrentAction(): Promise<void> { setState("mihome-vacuum.0.control.pause", true); // wait for the "paused" status before going home await waitForPauseOrSleep(); } async function stopCleanup(): Promise<void> { log(`Saugvorgang abgebrochen!`); setState(`Staubsauger.stop`, true, true); if (getState(idVacuumState).val !== 8) { await cancelCurrentAction(); await setStateAsync('mihome-vacuum.0.control.home', true); // wait for the "charging" status before resolving await waitFor(idVacuumState, 8); } log(`Staubsauger ist in der Basis`); setState(`Staubsauger.stop`, false, true); } async function cleanRoom(room: keyof typeof rooms): Promise<void> { log(`Saugvorgang für ${room} gestartet!`); setState(`Staubsauger.${room}`, true, true); const roomCoords = rooms[room]; const coords = roomCoords[0]; const targetCoords = [ ((coords[0] + coords[2]) / 2).toFixed(0), ((coords[1] + coords[3]) / 2).toFixed(0), ] // go to center of first zone const gotoString = targetCoords.join(","); await setStateAsync("mihome-vacuum.0.control.goTo", gotoString); log(`Fahre zur Mitte von ${room}`); await wait(10000); await waitForPauseOrSleep(); if (getState("Staubsauger.stop").val) return; const zoneCleanString = roomCoords.map(zone => { return "[" + zone.concat(1).map(coord => coord.toString()).join(",") + "]"; }).join(","); log("Starte Zonenreinigung..."); await setStateAsync("mihome-vacuum.0.control.zoneClean", zoneCleanString); // wait for the cleanup to finish await waitFor(idVacuumState, 6); } async function waitForPauseOrSleep(): Promise<void> { log("Warte auf Zustand schlafen oder Pause..."); switch (getState(idVacuumState).val) { case 3: case 10: log(" => Zustand bereits aktiv!"); return; default: await Promise.race([ waitFor(idVacuumState, 10), waitFor(idVacuumState, 3), ]); log(" => Zustand erreicht!"); } } function wait(ms: number): Promise<void> { return new Promise(resolve => { setTimeout(resolve, ms); }); } function waitFor(stateID: string, value: any): Promise<void> { return new Promise(resolve => { const handler = (obj: iobJS.ChangedStateObject) => { if (obj.newState.val === value) { unsubscribe(handler); resolve(); } } subscribe(stateID, handler); }) } function setStateAsync(id: string, state: any): Promise<void> { return new Promise(res => { setState(id, state, () => res()); }); }Beide Varianten legen für jeden Raum und für Stop einen State an, den du dann jeweils mit einer Alexa-Routine ansprechen kannst.
@AlCalzone Erstmal danke für dein Script
Leider scheint es bei mir nicht zu funktionieren, laut Log bleibt es bei
script.js.Xiaomi_Sauger.Zonenreinigung_Kartenrotation: Teste Kartenorientierung... stehen.
Ich nutze den mihome-vacuum 1.9.2 mit Valetudo RE 0.8.1, der Datenpunkt mapRotated bleibt auf unkown(-1). -
@AlCalzone Erstmal danke für dein Script
Leider scheint es bei mir nicht zu funktionieren, laut Log bleibt es bei
script.js.Xiaomi_Sauger.Zonenreinigung_Kartenrotation: Teste Kartenorientierung... stehen.
Ich nutze den mihome-vacuum 1.9.2 mit Valetudo RE 0.8.1, der Datenpunkt mapRotated bleibt auf unkown(-1). -
@Chris_71 sagte in Roborock S5 "cleaning area" per Tastendruck?:
Valetudo RE 0.8.1
Ich glaube das liegt an Valetudo RE 0.8.1. Dort bekommt man über /api/map/latest nichts mehr angezeigt, sondern es wird eine Datei ohne Endung heruntergeladen. Der Inhalt ist mit dem Notepad++ nicht lesbar.
Seit meinem Update von Valetudo auf Valetudo RE 0.8.1 habe ich das gleiche Problem. Siehe auch hier von @base im Beitrag hier