My Landroid-S Roboter Mower


(Matthias P) #1

hi,
I´m playing with openHAB since almost a year now. It became a real hobby for me, spending lots of hours learning linux basics, programming and soldering things together to customize my house automation. with the help from tons of tutorials and posts from this community together with my own trial and errors I got from an experimental playground to a fully operational home automation solution. my latest implementation is a roboter mower, which was on my christmas wishlist for many years. thanks to openhab it got prio 1.
the mower, we simply call “the Third”, is still in the socialising phase, meaning that family members need to learn to not let any tools or toys laying around on the lawn. our cats went from fear to curious to ignoring within a couple of days… this is the solution how I implemented our mower into openhab:

The mower:

I read lots of blogs and test comparison reports about various brands and at the end my choice fell on the Worx Landroid-S mower. It perfectly fits the size of my garden, it is small enough to reach thight spots behind some trees and it has a zonecontrol which allows me to navigate the mower to a small area behind the pool which is difficult to target by randomized movements. but the most important feature is the possibility to connect the mower to openhab.

The API:

someone developed a “bridge” for the Landroid-S mowers that allows status, statistics and commands to be sent and retrieved from the device:


I installed the bridge on the same raspberry Pi that also hosts openhab. the bridge can also integrate a weather provider but this didn`t work well for me, so I only used the basic functions. I use MQTT to communicate between openhab and bridge as mosquitto was already in place for other devices…

The functions:

the mower comes with an mobile app that gives you total control of the mower, so what is the added value of openhab?
the app has no notification service in the case that the mower needs attention. with openhab the mower can alert me when it´s stuck has lost or cut the guidance wire or fell down the cellar bay.
the app doesn´t tell the mower when it´s raining. so it will happen that the mower starts it´s job and comes back after 5 minutes because it´s rainsensor got wet. thanks to openhab it stays home and waits till the grass is dry before it starts to work again.
when kids are playing in the garden I can tell the mower to take off for the rest of the day.
another nice feature is that the mower now automatically adjusts the mowing time depending on the season of the year. as known grass grows faster in spring than in autum.
last but not least openhab can show various statistics like daily mowing time, charging times and required power.

The GUI:


The items:

Group gLandroid "Landi statistics"

String Landroid_DateTime "Last Update [%s]" <calendar> {mqtt="<[broker:landroid/status/dateTime:state:default]"}
Number Landroid_firmware "Firmware [v%s]" <lawnmower> {mqtt="<[broker:landroid/status/firmware:state:default]"}
Number Landroid_wifiQuality "Wifi Quality [SCALE(signalstrength.scale):%s]" <lawnmower> {mqtt="<[broker:landroid/status/wifiQuality:state:default]"}
String Landroid_active "Active [%s]"  <lawnmower> {mqtt="<[broker:landroid/status/active:state:default]"}
Number Landroid_rainDelay "rain Delay [%d min]"  <lawnmower> {mqtt="<[broker:landroid/status/rainDelay:state:default],
                                                                    >[broker:landroid/set/rainDelay:state:*:default]"}
Number Landroid_timeExtension "time Extension [%d %%]"  <lawnmower> {mqtt="<[broker:landroid/status/timeExtension:state:default],
                                                                           >[broker:landroid/set/timeExtension:state:*:default]"}
String Landroid_serialNumber "Serial Number [%d]"  <lawnmower> {mqtt="<[broker:landroid/status/serialNumber:state:default]"}
Number Landroid_totalTime "Total Time [JS(minutestohours.js):%s]"  <lawnmower> {mqtt="<[broker: landroid/status/totalTime:state:default]"}
Number:Length Landroid_totalDistance "Total Distance [%s m]"  <lawnmower> {mqtt="<[broker:landroid/status/totalDistance:state:default]"}     
Number Landroid_totalBladeTime "Total Bladetime [JS(minutestohours.js):%s]"  <lawnmower> {mqtt="<[broker:landroid/status/totalBladeTime:state:default]"}
Number Landroid_batteryChargeCycle "Battery ChargeCycle [%d]"  <lawnmower> {mqtt="<[broker:landroid/status/batteryChargeCycle:state:default]"}
String Landroid_batteryCharging "Battery charging [%s]" <lawnmower> {mqtt="<[broker:landroid/status/batteryCharging:state:default]"}
Number Landroid_batteryVoltage "Battery Voltage [%.2f V]"  <lawnmower> (gLandroid) {mqtt="<[broker:landroid/status/batteryVoltage:state:default]"}
Number Landroid_batteryTemperature "Battery Temperature [%.1f °C]" <lawnmower> (gLandroid) {mqtt="<[broker:landroid/status/batteryTemperature:state:default]"}
Number Landroid_batteryLevel "Battery Level [%d %%]"  <lawnmower> (gLandroid) {mqtt="<[broker:landroid/status/batteryLevel:state:default]"}
Number Landroid_errorCode "Error Code [MAP(landroid_errorcode.map):%s]"  <lawnmower> {mqtt="<[broker:landroid/status/errorCode:state:default]"}
String Landroid_ErrorDescription "Error [%s]" <lawnmower> {mqtt="<[broker:landroid/status/errorDescription:state:default]"}
Number Landroid_statusCode "Status Code [MAP(landroid_statuscode.map):%s]"  <lawnmower> {mqtt="<[broker:landroid/status/statusCode:state:default]"}
String Landroid_StatusDescription "Status [%s]" <sheep> {mqtt="<[broker:landroid/status/statusDescription:state:default]"}

//following items are for statistics and actions
Number Landi_mows (gLandroid) 
Number Landroidstatistic
String Landi_mowing 
Number Landroid_totalTime_yesterday  "totaltimeyesterday [%d]" <lawnmower>
Number Landroid_runTime_yesterday "Runtime yesterday [JS(minutestohours.js):%s]" <lawnmower>
Number:Length Landroid_totalDistance_yesterday "totaldistyesterday [%d]" <lawnmower>
Number:Length Landroid_Distance_yesterday "Distance yesterday [%.2f m]" <lawnmower>
Number Rem_Landroid_timeExtension 

The Rules:

       rule "Landroid out of control"
    when 
        Item Landroid_errorCode changed
            then
                if (Landroid_errorCode.state != 0) {
                    sendNotification("xxxx@gmx.at", "der III. meldet " + transform("MAP", "landroid_errorcode.map", Landroid_errorCode.state.toString))
                    }
    end


rule "for chart statistics" //to see when Landroid moves or sleeps
    when 
        Item Landroid_StatusDescription received update 
            then
                if (Landroid_StatusDescription.state.toString.contains("Mowing") || Condition.state.toString.contains("Cutting")) sendCommand(Landi_mows,100) 
                else if (Landroid_StatusDescription.state.toString.contains("Home")) sendCommand(Landi_mows,0)        
    end


rule "Landroid start/stop/sleep via mqtt"
    when 
        Item Landi_mowing received command
            then
                Thread::sleep(500)
                switch Landi_mowing.state {
                case "1": {
                        if (Landroid_timeExtension.state == -100) {
                            var Number RtE = Rem_Landroid_timeExtension.state as DecimalType
                            sendCommand(Landroid_timeExtension,RtE)
                        }
                        else if (Landroid_timeExtension.state > -100) {
                            logInfo("Landroid.rules","starten, mqttstart")
                            publish("broker", "landroid/set/start", "")
                        }
                        logInfo("Landroid.rules","landi_mowing: " + Landi_mowing.state)
                }
                case "0": {
                        var Number LtE = Landroid_timeExtension.state as DecimalType
                        sendCommand(Rem_Landroid_timeExtension,LtE)
                        publish("broker", "landroid/set/stop", "")
                        logInfo("Landroid.rules","landi_mowing: " + Landi_mowing.state)
                        }
                case "2": {
                    if (Landroid_timeExtension.state == -100) {
                        var Number RtE = Rem_Landroid_timeExtension.state as DecimalType
                        sendCommand(Landroid_timeExtension,RtE)
                        Landi_mowing.label = "der III., Status: " + Landroid_StatusDescription.state.toString
                        logInfo("Landroid.rules","Auszeit ausschalten, battery charging")
                    }
                    else if (Landroid_timeExtension.state > -100) {
                        var Number LtE = Landroid_timeExtension.state as DecimalType
                        publish("broker", "landroid/set/stop", "")
                        postUpdate(Rem_Landroid_timeExtension,LtE) 
                        sendCommand(Landroid_timeExtension,-100)
                        Landi_mowing.label = "der III., hat Auszeit!!!" 
                        logInfo("Landroid.rules","Auszeit einschalten, III go home")
                    }
                    logInfo("Landroid.rules","landi_mowing: " + Landi_mowing.state)
                }
                }
    end    


rule "label update"
    when
        Item Landroid_StatusDescription received update or
        Item Landroid_batteryLevel received update
            then
		        Thread::sleep(250)
                if (Landroid_batteryCharging.state.toString.contains("true") && Landi_mowing.state < 2) Landi_mowing.label = "der III., Charging: " + Landroid_batteryLevel.state.toString + "%"
                else if (Landroid_batteryCharging.state.toString.contains("false") && Landi_mowing.state < 2 && Raining.state == OFF) Landi_mowing.label = "der III.,: " + Landroid_StatusDescription.state.toString 
                else if (Landroid_batteryCharging.state.toString.contains("false") && Landi_mowing.state == 2 ) Landi_mowing.label = "der III., hat Auszeit!!!"  
    end


rule "runtime calc"
    when 
        Time cron "0 0 0 1/1 * ? *"  //00:00
            then
                val Number Lyt = Landroid_totalTime.state as Number
                postUpdate(Landroid_totalTime_yesterday, Lyt)
                val Number Lyd = Landroid_totalDistance.state as Number
                postUpdate(Landroid_totalDistance_yesterday, Lyd)
        end


rule "diff for runtime and distance"
    when
        Time cron "0 55 23 1/1 * ? *"  //23:55
            then
                val Number Ltt = Landroid_totalTime.state as Number
                val Number Ltty = Landroid_totalTime_yesterday.state as Number
                val Number rty = (Ltt - Ltty)
                postUpdate(Landroid_runTime_yesterday, rty)
                val Number Ltd = Landroid_totalDistance.state as Number
                val Number Ltdy = Landroid_totalDistance_yesterday.state as Number
                val Number dy = (Ltd - Ltdy)
                postUpdate(Landroid_Distance_yesterday, dy)    
    end


rule "remember that Landroid sleeps"
    when
        Time cron "0 0 8 1/1 * ? *"  //09:00 
            then
                logInfo("remember Landi sleeps","Landi_mowing: " + Landi_mowing.state)
                if (Landi_mowing.state == 2) {
                    sendNotification("xxxxx@gmx.at", "der III. ist immer noch im Schlafmodus!") 
                    }
    end    

rule "mowing time depening on season name"
    when   
        Item Season changed 
            then
                if (Season_Name.state.toString.contains("SPRING")) (Landroid_timeExtension.state == 50)
                else if (Season_Name.state.toString.contains("SUMMER")) (Landroid_timeExtension.state == 0)
                else if (Season_Name.state.toString.contains("AUTUMN")) (Landroid_timeExtension.state == -30)
    end        


rule "Landroid: it is raining"
    when
        Item Raining changed
            then
                Thread::sleep(250)
                var Number LtE = Landroid_timeExtension.state as DecimalType
                var Number rtE = Rem_Landroid_timeExtension.state as DecimalType
                switch (Raining.state.toString) {
                    case "ON": {if (Landi_mowing.state !=2) 
                        publish("broker", "landroid/set/stop", "")
                        postUpdate(Rem_Landroid_timeExtension,LtE) 
                        sendCommand(Landroid_timeExtension,-100)
                        Landi_mowing.label = "der III., ist kein Fisch!!!" 
                        logInfo("Landroid.rules","Es regnet, III go home")}
                    case "OFF": {if (Landi_mowing.state !=2) 
                        postUpdate(Landroid_timeExtension,rtE) 
                        Landi_mowing.label = "der III.,: " + Landroid_StatusDescription.state.toString
                        logInfo("Landroid.rules","Es regnet nicht mehr, III mach weiter")}
                }
    end


rule "switch rain on/off"
    when
        Item Condition changed
            then
                Thread::sleep(250)
                if (Condition.state.toString.contains("rain")) sendCommand(Raining,ON)
                else if (Raining.state == ON) {
                    var Timer rainTimer = null
                    rainTimer = createTimer(now.plusMinutes((Landroid_rainDelay.state as DecimalType).intValue), [|
                        if (Condition.state.toString.contains("rain")) sendCommand(Raining,ON)
                    //rainTimer.cancel
                        else (sendCommand(Raining,OFF))
                    logInfo("Landroid.rules","Es regnet seit 2h nicht mehr")])
                    }
    end



maybe my post gives someone else some ideas how to operate a roboter mower through openhab.

if
    someone finds any improvements in rules etc
       Then please don´t hesitate to tell me
       else if (any comments welcome!)
end

(scott dee) #2

this all looks great, i’m edging closer and closer to getting a landroid myself so this will be an excellent addition.
thanks


(Stefan Kühnen) #3

Hi Matthias,

thanks for your input. I currently setting up my Landroid with openHAB. Would you mind to publish your Landi sitemap?

Thanks and regards
Stefan


(Matthias P) #4

hi Stefan,
here we go…

Switch item=Landi_mowing icon="sheep" mappings=[0="Stoppen", 2="Auszeit"] visibility=[Landroid_StatusDescription == "Mowing"]
Switch item=Landi_mowing icon="sheep" mappings=[1="Starten", 2="Auszeit"] visibility=[Landroid_StatusDescription == "Home"]
Text item=Landroid_ErrorDescription icon="sheep" visibility=[Landroid_errorCode != 0 ]
Text item=Landroid_StatusDescription label="Statistics von der III." {
                    Text item=Landroid_DateTime 
                    Text item=Landroid_active
                    Text item=Landroid_firmware
                    Text item=Landroid_wifiQuality
                    Setpoint item=Landroid_rainDelay minValue=30 maxValue=180 step=10
                    Text item=Landroid_serialNumber
                    Text item=Landroid_totalTime
                    Text item=Landroid_runTime_yesterday
                    Text item=Landroid_totalDistance label="Total Distance [%.2f km]"
                    Text item=Landroid_Distance_yesterday label="Distance yesterday [%.2f km]" 
                    Text item=Landroid_totalBladeTime
                    Text item=Landroid_batteryChargeCycle
                    Text item=Landroid_batteryCharging
                    Text item=Landroid_batteryVoltage
                    Text item=Landroid_batteryTemperature
                    Text item=Landroid_batteryLevel
                    Text item=Landroid_errorCode
                    Text item=Landroid_ErrorDescription
                    Text item=Landroid_statusCode
                    Setpoint item=Landroid_timeExtension minValue=-100 maxValue=100 step=10
                    Switch item=PowerPlug1_Switch
                    Text item=PowerPlug1_Amp
                    Text item=PowerPlug1_kWh
                    Text item=PowerPlug1_watts
                    Text item=PowerPlug1_volts
                    Switch item=PowerPlug1_alarm
                    Switch item=Landroidstatistic label="Statistics" icon="line" mappings=[0="4h", 1="12h", 2="24h", 3="3 Tage"]
                    Chart item=gLandroid period=4h refresh=60000 legend=true visibility=[Landroidstatistic==0]
                    Chart item=gLandroid period=12h refresh=60000 legend=true visibility=[Landroidstatistic==1, Landroidstatistic=="NULL"]
                    Chart item=gLandroid period=D refresh=60000 legend=true visibility=[Landroidstatistic==2]
                    Chart item=gLandroid period=3D refresh=60000 legend=true visibility=[Landroidstatistic==3]
}

(BeNe) #5

Hi Matthias,

thanks for your great write up and examples. I´m at the same point with OpenHAB and MQTT :grinning:
Can you please publish your transformation files (landroid_error.map)?

Thank you!


(Matthias P) #6
0=0
1=Mäher festgefahren
2=Mäher angehoben
3=Draht fehlt
4=ausserh. der Begrenzung
5=wegen Regen gestoppt
8=Fehler Messermotor
9=Fehler Fahrmotor
11=Mäher umgekippt
12=Akku leer
13=Draht vertauscht
14=Akku Ladefehler
15=Station n. gefunden
16=Mäher gesperrt
17=Akkutemp. zu hoch 
NULL=undefiniert
undefined=undefiniert

(Garry Mitchell) #7

Thanks for sharing this - you have helped me make a decision on mower, and I’ve got a WR110MI on its way to me, and should be delivered tomorrow!

So that I can get statistics from day 1, I’m getting openHAB set up with this in advance! :wink:

Could you share your landdroid_statuscode.map, signalstrength.js and minutestohours.js files too? :slight_smile: Hopefully that’s the last pieces of the puzzle!!


(Matthias P) #8

great to hear!

landroid_statuscode.map:

0=In Station (Idle)
1=In Station (Home)
2=startet
3=fährt raus
4=fährt am Draht entlang
5=sucht die Station
6=sucht Begrenzungsdraht
7=Mähen
8=wurde angehoben
9=hat ein grobes Problem
10=Mähwerk blockiert
11=Debug
30=Heimfahrt
32=Kantenschnitt
33=Suche Mähbereich
34=Pause / gestoppt 
NULL=undefiniert
undefined=undefiniert

signalstrength.scale:

[..-90]=Wifi extrem schlecht
[-89..-80]=Wifi unzuverlässig
[-79..-70]=Wifi ausreichend
[-69..-67]=Wifi normal
[-66..-60]=Wifi gut
[-59..-50]=Wifi exzellent
[-49..0]=Wifi perfekt

minutestohours.js:

/*
Javascript transform function to change the number
of minutes of CPU time from the System Info Binding
into a more readable format
eg: 2365 into '1 day 15 hours 25 minutes

The item in the items file is defined as follow:
Number LocalComputer_Cpu_SystemUptime "[JS(CPUTime.js):%s]"
and linked via PaperUI to the System uptime channel
of the System Info Thing
*/

(function(i) {
    if (i == 'NULL') { return i; }
    if (i == '-') { return 'Undefined'; }
    var val = parseInt(i); // The value sent by OH is a string so we parse into an integer
    var days = 0; // Initialise variables
    var hours = 0;
    var minutes = 0;
    if (val >= 1440) { // 1440 minutes in a days
        days = Math.floor(val / 1440); // Number of days
        val = val - (days * 1440); // Remove days from val
    }
    if (val >= 60) { // 60 minutes in an hour
       hours = Math.floor(val /60); // Number of hours
        val = val - (hours * 60); // Remove hours from val
    }
    minutes = Math.floor(val); // Number of minutes

    var stringDays = ''; // Initialse string variables
    var stringHours = '';
    var stringMinutes = '';
    if (days === 1) {
        stringDays = '1 day '; // Only 1 day so no 's'
    } else if (days > 1) {
        stringDays = days + ' d '; // More than 1 day so 's'
    } // If days = 0 then stringDays remains ''

    if (hours === 1) {
        stringHours = '1 hour '; // Only 1 hour so no 's'
    } else if (hours > 1) {
        stringHours = hours + ' h '; // More than 1 hour so 's'
    } // If hours = 0 then stringHours remains ''

    if (minutes === 1) {
        stringMinutes = '1 minute'; // Only 1 minute so no 's'
    } else if (minutes > 1) {
        stringMinutes = minutes + ' min'; // More than 1 minute so 's'
    } // If minutes = 0 then stringMinutes remains ''

    var returnString =  stringDays + stringHours + stringMinutes
    return returnString.trim(); // Removes the extraneous space at the end

})(input)

(Oliver Libutzki) #9

I added a rule for telling the mower to stop working today:

Rules:

var Timer resetTimeExtensionTimer = null

rule "End of working day"
when 
    Item MowerActionControl received command "sleep"
then
    val currentTimeExtension = MowerTimeExtension.state
    if (currentTimeExtension > -100 && resetTimeExtensionTimer === null) {
        SamsonTimeExtension.sendCommand(-100)
        resetTimeExtensionTimer = createTimer(now.plusDays(1).withTimeAtStartOfDay) [
            MowerTimeExtension.sendCommand(0)
            MowerActionControl.postUpdate("")
            resetTimeExtensionTimer = null
        ]
    }
    publish("mqtt", "landroid/set/stop", "")
end

Items:

Number      MowerTimeExtension         "Time Extension [%d %%]"                          <lawnmower>         { mqtt="<[mqtt:landroid/status/timeExtension:state:default],>[mqtt:landroid/set/timeExtension:state:*:default]"}

String      MowerActionControl         "Aktion" 

Sitemap:

Switch item=MowerActionControl mappings=[sleep="End of working day"]

The time extension is set back to 0 at the start of the next day.


(Nicu BALEA) #10

Hi,

Thanks a lot for sharing this . Saved me a lot of time.
I set it up with landroid wr105si. With some small modifications, works like a charm :slight_smile: .
Some values don’t update, I didn’t found the reason yet (firmware, serial number, total distance, battery temperature) , but it’s no big deal. Hopefully I will find the error in the coming days.

regards,
Nicu.


(Matthias P) #11

check the “readings” tab in your bridge (http://localhost:3000). if your missing values are there then I guess the fault is somewhere in your item definitions…
let us know if your modifications are of interests for the community…
cheers…


(Matthias P) #12

just a mower related update:

The mower starts mowing based on your schedule but it does not consider the battery level at that time. worst case could be that it starts at 75% charged batteries. the rule solves that problem and makes sure that the mower only starts at >= 95%. make sure that the mower schedule is 30min after " now.getHourOfDay() >=19"

the same rule addresses a weird symptom. it happens from time to time that the mower turns rounds around a defined “island” until its batteries are finished. it just doesn`t find its homestation. there is no error for that, hence also no notification. the additional rule informs me when the battery is only 20% left as the mower normally returns home on 40% battery charge…

rule "battery low, III is lost. battery full, start working"
    when
        Item Landroid_batteryLevel changed
    then
        if (Landroid_batteryLevel.state <= 20 ) sendNotification("mxxxxxx@gmx.at", "der III. dreht sich im Kreis, Battery low!") 
        else if (now.getHourOfDay() >=19 && now.getHourOfDay() <=20 && Raining.state == OFF && Landroid_batteryLevel.state >=95) publish("broker", "landroid/set/start", "")
    end