NEWS

[Vorlage] Roborock Gen1 - Einzelraumreinigung mit Valetudo

  • Developer

    Hallo zusammen,

    ich habe dieses Skript schon mal an anderer Stelle gepostet. Der Roborock Generation 1 hat ja das Problem, dass er gerne mal die Karte dreht und somit statische Skripte nicht funktionieren, wenn man die Zonenreinigung als Einzelraumreinigung nutzen möchte.

    Das Problem habe ich mit der alternativen Firmware Valetudo umgangen, bei der man anhand der Kartendaten auslesen kann, wie und ob die Karte gedreht wurde.

    Die Koordinaten sind kompatibel zum mihome-vacuum Adapter und damit gegenüber den Koordinaten aus Valetudo vertikal gespiegelt. Zum Umrechnen müssen die y-Koordinaten folgendermaßen umgerechnet werden: y(skript) = 51200 - y(Valetudo)?


    Version 1: funktioniert bis Firmware Valetudo 0.4.0-RE5
    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());
        });
    }
    

    Version 2: Wie Version 1, nur ohne Prüfung der automatischen Rotation. Benötigt kein Valetudo auf dem Sauger:

    // 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.


    Version 3: Wie 1, funktioniert ab Firmware Valetudo 0.4.0-RE6. Zusätzlich muss das Modul "pako" in den Adapter-Einstellungen geladen werden.

    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 (mapRotation === -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`,
    });
    let mapRotation: number = -1;
    if (existsState(idMapRotated)) {
        const rotation = getState(idMapRotated).val;
        if (rotation != undefined) mapRotation = rotation;
    }
    on(idMapRotated, obj => {
        mapRotation = obj.state.val;
    });
    
    /** 
     * 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 binaryData = await axios({
            url: `http://${roboHostname}/api/map/latest`,
            auth: valetudoAuth,
            responseType: "arraybuffer"
        });
    
        // Decode map data
        let charger;
        let robot;
        try {
            const parsed = RRMapParser.PARSEDATA(
                Buffer.from(
                    require("pako").inflate(binaryData.data)
                )
            );
            ({ charger, robot } = parsed);
        } catch (e) {
            log(`Error decoding map data: ${e.message}`, "warn");
            return 0;
        }
    
        // 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> {
        mapRotation = rotation;
        return setStateAsync(idMapRotated, rotation);
    }
    
    // "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 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());
        });
    }
    
    
    // === binary map parser from rand256/valetudo
    
    const Tools = {
        DIMENSION_PIXELS: 1024,
        DIMENSION_MM: 50 * 1024
    };
    
    interface Pixels {
        floor: number[],
        obstacle: number[],
        segments: number[],
    }
    
    interface ParsedMapData {
        image?: any;
        charger?: any;
        robot?: any;
        goto_target?: any;
        currently_cleaned_zones?: any;
        forbidden_zones?: any;
        virtual_walls?: any;
        currently_cleaned_blocks?: any;
    }
    
    const RRMapParser = {
    
        TYPES: {
            "CHARGER_LOCATION": 1,
            "IMAGE": 2,
            "PATH": 3,
            "GOTO_PATH": 4,
            "GOTO_PREDICTED_PATH": 5,
            "CURRENTLY_CLEANED_ZONES": 6,
            "GOTO_TARGET": 7,
            "ROBOT_POSITION": 8,
            "FORBIDDEN_ZONES": 9,
            "VIRTUAL_WALLS": 10,
            "CURRENTLY_CLEANED_BLOCKS": 11,
            "DIGEST": 1024
        },
    
        PARSEBLOCK: function parseBlock(buf, offset, result?) {
            result = result || {};
            if (buf.length <= offset) {
                return result;
            }
            let g3offset = 0;
            var type = buf.readUInt16LE(0x00 + offset),
                hlength = buf.readUInt16LE(0x02 + offset),
                length = buf.readUInt32LE(0x04 + offset);
            switch (type) {
                case RRMapParser.TYPES.ROBOT_POSITION:
                case RRMapParser.TYPES.CHARGER_LOCATION:
                    result[type] = {
                        position: [
                            buf.readUInt16LE(0x08 + offset),
                            buf.readUInt16LE(0x0c + offset)
                        ],
                        angle: length >= 12 ? buf.readInt32LE(0x10 + offset) : 0 // gen3+
                    };
                    break;
                case RRMapParser.TYPES.IMAGE:
                    if (hlength > 24) { // gen3+
                        g3offset = 4;
                    }
                    const parameters = {
                        segments: {
                            count: g3offset ? buf.readInt32LE(0x08 + offset) : 0,
                            id: [],
                        },
                        position: {
                            top: buf.readInt32LE(0x08 + g3offset + offset),
                            left: buf.readInt32LE(0x0c + g3offset + offset)
                        },
                        dimensions: {
                            height: buf.readInt32LE(0x10 + g3offset + offset),
                            width: buf.readInt32LE(0x14 + g3offset + offset)
                        },
                        pixels: {} as Pixels
                    };
                    parameters.position.top = Tools.DIMENSION_PIXELS - parameters.position.top - parameters.dimensions.height;
                    if (parameters.dimensions.height > 0 && parameters.dimensions.width > 0) {
                        parameters.pixels = {
                            floor: [],
                            obstacle: [],
                            segments: [],
                        };
                        for (let s, i = 0; i < length; i++) {
                            switch (buf.readUInt8(0x18 + g3offset + offset + i) & 0x07) {
                                case 0:
                                    break;
                                case 1:
                                    parameters.pixels.obstacle.push(i);
                                    break;
                                default:
                                    parameters.pixels.floor.push(i);
                                    s = (buf.readUInt8(0x18 + g3offset + offset + i) & 248) >> 3;
                                    if (s !== 0) {
                                        if (!parameters.segments.id.includes(s)) parameters.segments.id.push(s);
                                        parameters.pixels.segments.push(i | (s << 21));
                                    }
                                    break;
                            }
                        }
                    }
                    result[type] = parameters;
                    break;
                case RRMapParser.TYPES.PATH:
                case RRMapParser.TYPES.GOTO_PATH:
                case RRMapParser.TYPES.GOTO_PREDICTED_PATH:
                    const points = [];
                    for (let i = 0; i < length; i = i + 4) {
                        //to draw these coordinates onto the map pixels, they have to be divided by 50
                        points.push([
                            buf.readUInt16LE(0x14 + offset + i),
                            buf.readUInt16LE(0x14 + offset + i + 2)
                        ]);
                    }
                    result[type] = {
                        //point_count: buf.readUInt32LE(0x08 + offset),
                        //point_size: buf.readUInt32LE(0x0c + offset),
                        current_angle: buf.readUInt32LE(0x10 + offset), //This is always 0. Roborock didn't bother
                        points: points
                    };
                    break;
                case RRMapParser.TYPES.GOTO_TARGET:
                    result[type] = {
                        position: [
                            buf.readUInt16LE(0x08 + offset),
                            buf.readUInt16LE(0x0a + offset)
                        ]
                    };
                    break;
                case RRMapParser.TYPES.CURRENTLY_CLEANED_ZONES:
                    const zoneCount = buf.readUInt32LE(0x08 + offset);
                    const zones = [];
                    if (zoneCount > 0) {
                        for (let i = 0; i < length; i = i + 8) {
                            zones.push([
                                buf.readUInt16LE(0x0c + offset + i),
                                buf.readUInt16LE(0x0c + offset + i + 2),
                                buf.readUInt16LE(0x0c + offset + i + 4),
                                buf.readUInt16LE(0x0c + offset + i + 6)
                            ]);
                        }
    
                        result[type] = zones;
                    }
                    break;
                case RRMapParser.TYPES.FORBIDDEN_ZONES:
                    const forbiddenZoneCount = buf.readUInt32LE(0x08 + offset);
                    const forbiddenZones = [];
                    if (forbiddenZoneCount > 0) {
                        for (let i = 0; i < length; i = i + 16) {
                            forbiddenZones.push([
                                buf.readUInt16LE(0x0c + offset + i),
                                buf.readUInt16LE(0x0c + offset + i + 2),
                                buf.readUInt16LE(0x0c + offset + i + 4),
                                buf.readUInt16LE(0x0c + offset + i + 6),
                                buf.readUInt16LE(0x0c + offset + i + 8),
                                buf.readUInt16LE(0x0c + offset + i + 10),
                                buf.readUInt16LE(0x0c + offset + i + 12),
                                buf.readUInt16LE(0x0c + offset + i + 14)
                            ]);
                        }
    
                        result[type] = forbiddenZones;
                    }
                    break;
                case RRMapParser.TYPES.VIRTUAL_WALLS:
                    const wallCount = buf.readUInt32LE(0x08 + offset);
                    const walls = [];
                    if (wallCount > 0) {
                        for (let i = 0; i < length; i = i + 8) {
                            walls.push([
                                buf.readUInt16LE(0x0c + offset + i),
                                buf.readUInt16LE(0x0c + offset + i + 2),
                                buf.readUInt16LE(0x0c + offset + i + 4),
                                buf.readUInt16LE(0x0c + offset + i + 6)
                            ]);
                        }
    
                        result[type] = walls
                    }
                    break;
                case RRMapParser.TYPES.CURRENTLY_CLEANED_BLOCKS:
                    const blockCount = buf.readUInt32LE(0x08 + offset);
                    const blocks = [];
                    if (blockCount > 0) {
                        for (let i = 0; i < length; i++) {
                            blocks.push(buf.readUInt8(0x0c + offset + i));
                        }
                        result[type] = blocks;
                    }
                    break;
            }
            return parseBlock(buf, offset + length + hlength, result);
        },
    
        /**
         *
         * @param mapBuf {Buffer} Should contain map in RRMap Format
         * @return {object}
         */
        PARSE: function parse(mapBuf) {
            if (mapBuf[0x00] === 0x72 && mapBuf[0x01] === 0x72) {// rr
                const parsedMapData = {
                    header_length: mapBuf.readUInt16LE(0x02),
                    data_length: mapBuf.readUInt16LE(0x04),
                    version: {
                        major: mapBuf.readUInt16LE(0x08),
                        minor: mapBuf.readUInt16LE(0x0A)
                    },
                    map_index: mapBuf.readUInt16LE(0x0C),
                    map_sequence: mapBuf.readUInt16LE(0x10)
                };
                return parsedMapData;
            } else {
                return {};
            }
        },
    
        PARSEDATA: function parseData(mapBuf) {
            if (!this.PARSE(mapBuf).map_index) {
                return null;
            }
            const blocks = RRMapParser.PARSEBLOCK(mapBuf, 0x14);
            const parsedMapData: ParsedMapData = {};
            if (blocks[RRMapParser.TYPES.IMAGE]) { //We need the image to flip everything else correctly
                parsedMapData.image = blocks[RRMapParser.TYPES.IMAGE];
                [
                    {
                        type: RRMapParser.TYPES.PATH,
                        path: "path"
                    },
                    {
                        type: RRMapParser.TYPES.GOTO_PREDICTED_PATH,
                        path: "goto_predicted_path"
                    },
                ].forEach(item => {
                    if (blocks[item.type]) {
                        parsedMapData[item.path] = blocks[item.type];
                        parsedMapData[item.path].points = parsedMapData[item.path].points.map(point => {
                            point[1] = Tools.DIMENSION_MM - point[1];
                            return point;
                        });
    
                        if (parsedMapData[item.path].points.length >= 2) {
                            parsedMapData[item.path].current_angle =
                                Math.atan2(
                                    parsedMapData[item.path].points[parsedMapData[item.path].points.length - 1][1] -
                                    parsedMapData[item.path].points[parsedMapData[item.path].points.length - 2][1],
    
                                    parsedMapData[item.path].points[parsedMapData[item.path].points.length - 1][0] -
                                    parsedMapData[item.path].points[parsedMapData[item.path].points.length - 2][0]
    
                                ) * 180 / Math.PI;
                        }
                    }
                });
                if (blocks[RRMapParser.TYPES.CHARGER_LOCATION]) {
                    parsedMapData.charger = blocks[RRMapParser.TYPES.CHARGER_LOCATION].position;
                    parsedMapData.charger[1] = Tools.DIMENSION_MM - parsedMapData.charger[1];
                }
                if (blocks[RRMapParser.TYPES.ROBOT_POSITION]) {
                    parsedMapData.robot = blocks[RRMapParser.TYPES.ROBOT_POSITION].position;
                    parsedMapData.robot[1] = Tools.DIMENSION_MM - parsedMapData.robot[1];
                }
                if (blocks[RRMapParser.TYPES.GOTO_TARGET]) {
                    parsedMapData.goto_target = blocks[RRMapParser.TYPES.GOTO_TARGET].position;
                    parsedMapData.goto_target[1] = Tools.DIMENSION_MM - parsedMapData.goto_target[1];
                }
                if (blocks[RRMapParser.TYPES.CURRENTLY_CLEANED_ZONES]) {
                    parsedMapData.currently_cleaned_zones = blocks[RRMapParser.TYPES.CURRENTLY_CLEANED_ZONES];
                    parsedMapData.currently_cleaned_zones = parsedMapData.currently_cleaned_zones.map(zone => {
                        zone[1] = Tools.DIMENSION_MM - zone[1];
                        zone[3] = Tools.DIMENSION_MM - zone[3];
    
                        return zone;
                    });
                }
                if (blocks[RRMapParser.TYPES.FORBIDDEN_ZONES]) {
                    parsedMapData.forbidden_zones = blocks[RRMapParser.TYPES.FORBIDDEN_ZONES];
                    parsedMapData.forbidden_zones = parsedMapData.forbidden_zones.map(zone => {
                        zone[1] = Tools.DIMENSION_MM - zone[1];
                        zone[3] = Tools.DIMENSION_MM - zone[3];
                        zone[5] = Tools.DIMENSION_MM - zone[5];
                        zone[7] = Tools.DIMENSION_MM - zone[7];
    
                        return zone;
                    })
                }
                if (blocks[RRMapParser.TYPES.VIRTUAL_WALLS]) {
                    parsedMapData.virtual_walls = blocks[RRMapParser.TYPES.VIRTUAL_WALLS];
                    parsedMapData.virtual_walls = parsedMapData.virtual_walls.map(wall => {
                        wall[1] = Tools.DIMENSION_MM - wall[1];
                        wall[3] = Tools.DIMENSION_MM - wall[3];
    
                        return wall;
                    });
                }
                if (blocks[RRMapParser.TYPES.CURRENTLY_CLEANED_BLOCKS]) {
                    parsedMapData.currently_cleaned_blocks = blocks[RRMapParser.TYPES.CURRENTLY_CLEANED_BLOCKS];
                }
            }
            return parsedMapData;
        },
    
    };
    
  • Starter

    @AlCalzone

    Danke für deine Mühe das Thema nachhaltig zu lösen.

    Ich habe heute nun endlich meinen Gen1 mit dem aktuellsten Valetudo Image aufgesetzt.

    Nun bin ich dabei, den ioBroker anzubinden. Hier hab ich jedoch noch Probleme.

    Ich habe via npm axios & pako nachinstalliert.

    Jedoch scheint er schon in Zeile 1 ein Problem zu bekommen:

    javascript.0	2020-03-22 21:40:28.584	error	(1199) at process._tickCallback (internal/process/next_tick.js:68:7)
    javascript.0	2020-03-22 21:40:28.584	error	(1199) at promise.then (/opt/iobroker/node_modules/standard-as-callback/built/index.js:19:49)
    javascript.0	2020-03-22 21:40:28.584	error	(1199) at tryCatcher (/opt/iobroker/node_modules/standard-as-callback/built/utils.js:11:23)
    javascript.0	2020-03-22 21:40:28.584	error	(1199) at client.get (/opt/iobroker/node_modules/iobroker.js-controller/lib/states/statesInRedis.js:491:17)
    javascript.0	2020-03-22 21:40:28.584	error	(1199) at adapter.getForeignState (/opt/iobroker/node_modules/iobroker.javascript/main.js:745:17)
    javascript.0	2020-03-22 21:40:28.583	error	(1199) at createProblemObject (/opt/iobroker/node_modules/iobroker.javascript/main.js:1123:17)
    javascript.0	2020-03-22 21:40:28.583	error	(1199) at prepareScript (/opt/iobroker/node_modules/iobroker.javascript/main.js:1070:37)
    javascript.0	2020-03-22 21:40:28.583	error	(1199) at compile (/opt/iobroker/node_modules/iobroker.javascript/main.js:878:28)
    javascript.0	2020-03-22 21:40:28.583	error	(1199) at Object.createScript (vm.js:277:10)
    javascript.0	2020-03-22 21:40:28.583	error	(1199) at new Script (vm.js:83:7)
    javascript.0	2020-03-22 21:40:28.582	error	(1199) SyntaxError: Unexpected identifier
    javascript.0	2020-03-22 21:40:28.582	error	(1199) ^^^^^
    javascript.0	2020-03-22 21:40:28.582	error	(1199) import axios from "axios";
    javascript.0	2020-03-22 21:40:28.582	error	at script.js.Eigene_Scripte_-_Tests.Robbie_Map_Rotation:1
    javascript.0	2020-03-22 21:40:28.582	error	(1199) script.js.Eigene_Scripte_-_Tests.Robbie_Map_Rotation compile failed:
    

    Darüber hinaus hätte ich eine Frage zum Winkel. Mein Vacuum steht "unter" der Ladestation. Südlich sozusagen.

    Muss ich hier noch etwas im Script anpassen?

    Ich habe bisher eine Zone "Haus" angelegt. Diese hat die Koordinaten, wie ich sie auch mit der Original-FW genutzt hatte.

    Danke für deine Hilfe!

    VG!

  • Developer

    @ntsa86 Du hast das Skript als JavaScript, nicht wie oben steht als TypeScript angelegt.

  • Starter

    @AlCalzone

    Ja, wer lesen kann ... danke.

    Musste mein ioBroker erstmal auf den aktuellen Stand bringen. Dann klappt’s auch mit den Modulen.

    Hätte noch eine Frage zu den Koordinaten. Ich bekomme nun den Status -1 unknown

    Wo muss ich was eintragen, damit das Script erkennt, in welcher Ausrichtung der Robbi zur Station steht?

    VG!

  • Developer

    @ntsa86 Dazu musst du eigentlich nur eine Einzelraumreinigung starten. Beim Start checkt das Skript die Ausrichtung, sofern du mindestens 1x kurz den Sauger an hattest, um einen Teil der Karte aufzubauen.

  • Starter

    @AlCalzone

    Ok habe ich eben nochmal gestartet (Script erkannte: 90°). Dabei dreht er meine Zonen nun um 90° - muss somit wohl die Koordinaten anpassen. Habe diese direkt aus Valetudo rausgenommen (über http://<ip>/api/get_config).

    Somit wird dann aus

    "Haus": [[21721,21190,28482,32195] -> Valetudo
    "Haus": [[29479,30010,2271819005] -> 51200 - "Valetudo-Korrdinaten"

    Hab ich gemacht. Ging schief. 🙂

    Die originalen Valetudo Zonen sehen wie folgt aus:

    haus_valetudo_zone.PNG

    Das Script macht dann daraus:

    haus_script_zone.PNG

    Was mach ich falsch?

    VG & Danke für deine Geduld. 🙂

  • Developer

    @ntsa86 Das Skript geht davon aus, dass der Roboter normalerweise links steht. Könnte ich ggf noch anpassbar machen.

    Ändere bitte folgende Zeilen bei dir (angenommen du verwendest Version 3):

    Zeilen 95-98:

            "0": "Robo unten vom Dock",
            "90": "Robo rechts vom Dock",
            "180": "Robo oben vom Dock",
            "270": "Robo links vom Dock",
    

    Zeilen 142-153:

        if (angle <= 45 && angle >= -45) {
            // Sauger steht rechts
            return 90;
        } else if (angle > 45 && angle < 135) {
            // Sauger steht oben
            return 180;
        } else if (angle < -45 && angle > -135) {
            // Sauger steht unten
            return 0;
        } else {
            // Sauger steht links
            return 270;
        }
    
  • Developer

    @ntsa86 sagte in [Vorlage] Roborock Gen1 - Einzelraumreinigung mit Valetudo:

    [[29479,30010,2271819005] -> 51200 - "Valetudo-Korrdinaten"

    Und das gilt nur für die y-Werte, also den 2. und 4.
    Die x-Koordinaten darfst du nicht anfassen!

  • Starter

    @AlCalzone Danke, jetzt hat es geklappt!


  • Hallo,

    habe Valetudo RE 0.8.1 auf meinem Gen1. Kann mir jemand sagen wie ich dort die Koordinaten meiner angelegten Zonen auslesen kann, damit ich diese im Skript eintragen kann?

  • Developer

    @0018 Ich weiß nicht ob es einen einfachereren Weg gibt, aber wenn du eine Zone anlegst, anschließend die Developer-Tools (F12) öffnest und dort die Netzwerkanalyse startest, dann die Seite neu lädst, gibt es eine Anfrage an "get_config". Dort sind die Zonen enthalten:
    d16a2cc2-9f81-4612-8156-04f75de3c8b5-grafik.png

  • Starter

    @0018

    Zonen in Valetudo anlegen und dann im Browser http://<ip>/api/get_config aufrufen. Natürlich die IP deines Robbi verwenden.


  • @ntsa86 sagte in [Vorlage] Roborock Gen1 - Einzelraumreinigung mit Valetudo:

    http://<ip>/api/get_config

    Wunderbar, hat direkt geklappt! Danke 👍

  • Starter

    @AlCalzone

    Zu Früh gefreut. Er kommt mit den GoTo nicht zurecht. Die Zonen werden aber richtig ausgerichtet. Gibts die Möglichkeit den GoTo Befehl auszukommentieren (zum Test).

    Aktuell fährt er nur rum und sucht seinen Punkt:

    7B1A99EB-0791-4A9A-AA85-50B9FD3660F1.jpeg

  • Developer

    @ntsa86 Der GoTo-Punkt liegt immer in der Mitte der Zone (bzw. des ersten Abschnitts). Vermutlich ist das bei dir hinter einer Wand. Kommentiere folgende Zeilen aus:
    bf1b5490-c121-47fc-8187-8e95272eee6c-grafik.png


  • Moin!

    Ich habe es heute endlich geschafft, Valetudo auf meinen Vacuum v1 zu bekommen.
    So weit so gut....

    Leider habe ich Probleme mit dem Script - es wird folgender Fehler ausgegeben:

    javascript.0 (30625) script.js.Test.Rockrobo2: TypeScript compilation failed: import axios from "axios"; ^ ERROR: Cannot find module 'axios'.
    

    Nun meine vllt. etwas doofe Frage:
    Wo muss ich dieses Modul installieren?
    Habe mich über ssh beim iobroker eingeloggt und mit

    npm install axios -S
    

    eigentlich (so dachte ich) axios installiert.

    Habe ich da etwas falsch verstanden?

    Edit: Im Javascript-Adapter habe ich unter "Zusätzliche NPM-Module" auch axios eingetragen. Hilft trotzdem nix....

    Grüße!

  • Developer

    @TemPeck sagte in [Vorlage] Roborock Gen1 - Einzelraumreinigung mit Valetudo:

    eigentlich (so dachte ich) axios installiert.

    So nicht, so:

    Im Javascript-Adapter habe ich unter "Zusätzliche NPM-Module" auch axios eingetragen

    Eigentlich müsste es so funktionieren. Hast du bei der richtigen Instanz installiert? Gewartet, bis die Installation im Log als abgeschlossen steht?


  • Ich habe nur eine Javascript-Instanz. Und laut log müsste es auch installiert sein:

    2020-03-28 22:20:37.561 - info: host.iobroker instance system.adapter.javascript.0 started with pid 30625
    2020-03-28 22:20:40.422 - info: javascript.0 (30625) starting. Version 4.4.3 in /opt/iobroker/node_modules/iobroker.javascript, node: v10.17.0
    2020-03-28 22:20:40.454 - info: javascript.0 (30625) npm install axios --production --prefix "/opt/iobroker/node_modules/iobroker.javascript" (System call)
    2020-03-28 22:20:42.755 - info: javascript.0 (30625) + axios@0.19.2
    added 4 packages from 7 contributors and audited 29 packages in 1.399s
    2020-03-28 22:20:42.756 - info: javascript.0 (30625) found 0 vulnerabilities
    2020-03-28 22:20:42.811 - info: javascript.0 (30625) requesting all states
    2020-03-28 22:20:42.813 - info: javascript.0 (30625) requesting all objects
    

    Meinst du, dass die Installation über "npm install axios -S" irgendwelche Probleme verursacht haben könnte?

  • Developer

    @TemPeck Ich vermute eher, dass TypeScript sich an etwas verschluckt hat - obwohl die Fixes schon länger im Adapter enthalten sein müssten. Setz mal //@ts-ignore über die Import-Zeile.


  • Erledigt - nun taucht kein Fehler auf.

    16:06:26.443	info	javascript.0 (1227) script.js.Test.Rockrobo2: compiling TypeScript source...
    16:06:26.599	info	javascript.0 (1227) script.js.Test.Rockrobo2: TypeScript compilation successful
    16:06:26.611	info	javascript.0 (1227) script.js.Test.Rockrobo2: registered 7 subscriptions and 0 schedules
    

    Ob alles klappt, kann ich erst später testen.

    Vielen Dank für den Support!

Suggested Topics

  • 45
  • 11
  • 4
  • 1126
  • 1
  • 3
  • 25
  • 17

2.3k
Online

35.2k
Users

41.1k
Topics

566.1k
Posts