diff --git a/CODEOWNERS b/CODEOWNERS index 7ca2d5d362f02..da38d0ee73337 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -442,6 +442,7 @@ /bundles/org.openhab.binding.wlanthermo/ @CSchlipp /bundles/org.openhab.binding.wled/ @Skinah /bundles/org.openhab.binding.wolfsmartset/ @BoBiene +/bundles/org.openhab.binding.worxlandroid/ @clinique /bundles/org.openhab.binding.wundergroundupdatereceiver/ @danieldemus /bundles/org.openhab.binding.x/ @computergeek1507 /bundles/org.openhab.binding.xmltv/ @clinique diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 2aa407838123e..e954b8c268d8d 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -2186,6 +2186,11 @@ org.openhab.binding.wolfsmartset ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.worxlandroid + ${project.version} + org.openhab.addons.bundles org.openhab.binding.wundergroundupdatereceiver diff --git a/bundles/org.openhab.binding.worxlandroid/NOTICE b/bundles/org.openhab.binding.worxlandroid/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.binding.worxlandroid/README.md b/bundles/org.openhab.binding.worxlandroid/README.md new file mode 100644 index 0000000000000..c2c3bc95d26cb --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/README.md @@ -0,0 +1,526 @@ + +# WorxLandroid Binding + +This is the binding for Worx Landroid robotic lawn mowers. +It connects openHAB with your WorxLandroid mower using the API and MQTT. +This binding allows you to integrate, view and control supported Worx lawn mowers with openHAB. + +## Supported Things + +Currently following Things are supported: + +- `bridge`: **Bridge Worx Landroid API** Thing representing the handler for Worx API +- `mower`: One or many Things for supported **Landroid Mower**'s + +## Discovery + +A Bridge is required to connect to the Worx API. +Here you can provide your credentials for your WorxLandroid account. +Once the Bridge has been added Worx Landroid mowers will be discovered automatically. + +## Things Configuration + +The following options can be set for the `bridge`: + +| Property | Description | +|----------|------------------------------------------| +| username | Username to access the WorxLandroid API. | +| password | Password to access the WorxLandroid API. | + +The following options can be set for the `mower`: + +| Property | Description | Default | Advanced | +|-----------------------|----------------------------------------------------------------------------------------------|---------|----------| +| serialNumber | Serial number of the mower | - | No | +| refreshStatusInterval | Interval for refreshing mower status (ONLINE/OFFLINE) and channel 'common#online' in seconds | 3600 | Yes | +| pollingInterval | Interval for polling in seconds (min="30" max="7200"). | 1200 | Yes | + + +Default values for `refreshStatusInterval` and `pollingInterval` are the recommended settings in order to prevent a 24h ban from Worx. +Lower polling and refresh values will likely result in a 24h ban for your account. + +## Channels + +Currently following **Channels** are supported on the **Landroid Mower**: + +### Common + +| Channel | Type | ChannelName | Values | +|------------------|----------|-------------------------|-------------------| +| status | String | common#status | *1 (see below) | +| error | String | common#error | *2 (see below) | +| online | Switch | common#online | | +| online-timestamp | DateTime | common#online-timestamp | | +| action | String | common#action | START, STOP, HOME | +| enable | Switch | common#enable | | +| lock | Switch | common#lock | | + +*1: Values for **error** Channel: + +UNKNOWN, NO_ERR, TRAPPED, LIFTED, WIRE_MISSING, OUTSIDE_WIRE, RAINING, CLOSE_DOOR_TO_MOW, CLOSE_DOOR_TO_GO_HOME, BLADE_MOTOR_BLOCKED, WHEEL_MOTOR_BLOCKED, TRAPPED_TIMEOUT, UPSIDE_DOWN, BATTERY_LOW, REVERSE_WIRE, CHARGE_ERROR, TIMEOUT_FINDING_HOME, MOWER_LOCKED, BATTERY_OVER_TEMPERATURE, MOWER_OUTSIDE_WIRE + +*2: Values for **status** Channel: + +UNKNOWN, IDLE, HOME, START_SEQUENCE, LEAVING_HOME, FOLLOW_WIRE, SEARCHING_HOME, SEARCHING_WIRE, MOWING, LIFTED, TRAPPED, BLADE_BLOCKED, DEBUG, REMOTE_CONTROL, GOING_HOME, ZONE_TRAINING, BORDER_CUT, SEARCHING_ZONE, PAUSE, MANUAL_STOP + +### Config + +| Channel | Type | ChannelName | +|-----------|----------|------------------| +| timestamp | DateTime | config#timestamp | +| command | Number | config#command | + +### Multi-Zones + +If Multi Zones are supported, you are able to define 4 separate zones and split working times by 10 to those. + +To ease zone configuration, you are able to set distance in meters where a specific zone starts. Bearing in mind that you roughly shall know how many meters of cable have been used (without buffer). + +As second step you are able to set time in percent and split in parts of 10 between allocation zones. + +| Channel | Type | ChannelName | +|--------------|---------------|--------------------------| +| enable | Switch | multi-zones#enable | +| last-zone | Number | multi-zones#last-zone | +| zone-1 | Number:Length | multi-zones#zone-1 | +| zone-2 | Number:Length | multi-zones#zone-2 | +| zone-3 | Number:Length | multi-zones#zone-3 | +| zone-4 | Number:Length | multi-zones#zone-4 | +| allocation-0 | Number | multi-zones#allocation-0 | +| allocation-1 | Number | multi-zones#allocation-1 | +| allocation-2 | Number | multi-zones#allocation-2 | +| allocation-3 | Number | multi-zones#allocation-3 | +| allocation-4 | Number | multi-zones#allocation-4 | +| allocation-5 | Number | multi-zones#allocation-5 | +| allocation-6 | Number | multi-zones#allocation-6 | +| allocation-7 | Number | multi-zones#allocation-7 | +| allocation-8 | Number | multi-zones#allocation-8 | +| allocation-9 | Number | multi-zones#allocation-9 | + +### Schedule + +| Channel | Type | ChannelName | | +|----------------|----------|-------------------------|-------------------| +| mode | String | schedule#mode | ONLY IF SUPPORTED | +| time-extension | Number | schedule#time-extension | | +| next-start | DateTime | schedule#next-start | | +| next-stop | DateTime | schedule#next-stop | | + +### Aws + +| Channel | Type | ChannelName | +|-----------|--------|---------------| +| poll | Switch | aws#poll | +| connected | Switch | aws#connected | + +### Sunday (Slot 1) + +| Channel | Type | ChannelName | +|----------|-------------|-----------------| +| enable | Switch | sunday#enable | +| time | DateTime | sunday#time | +| duration | Number:Time | sunday#duration | +| edgecut | Switch | sunday#edgecut | + +### Sunday2 (Slot 2, ONLY IF SUPPORTED) + +| Channel | Type | ChannelName | +|----------|-------------|------------------| +| enable | Switch | sunday2#enable | +| time | DateTime | sunday2#time | +| duration | Number:Time | sunday2#duration | +| edgecut | Switch | sunday2#edgecut | + +And so on for each day of the week along with the Slot 2 when supported. + +### One-Time + +| Channel | Type | ChannelName | +|----------|--------|-------------------| +| edgecut | Switch | one-time#edgecut | +| duration | Switch | one-time#duration | + +### Battery + +| Channel | Type | ChannelName | +|---------------------|--------------------------|-----------------------------| +| temperature | Number:Temperature | battery#temperature | +| voltage | Number:ElectricPotential | battery#voltage | +| level | Number | battery#level | +| charge-cycles | Number | battery#charge-cycles | +| charge-cycles-total | Number | battery#charge-cycles-total | +| charging | Switch | battery#charging | + +### Orientation + +| Channel | Type | ChannelName | +|---------|--------------|-------------------| +| pitch | Number:Angle | orientation#pitch | +| roll | Number:Angle | orientation#roll | +| yaw | Number:Angle | orientation#yaw | + +### Metrics + +| Channel | Type | ChannelName | +|------------------|---------------|--------------------------| +| blade-time | Number:Time | metrics#blade-time | +| blade-time-total | Number:Time | metrics#blade-time-total | +| distance | Number:Length | metrics#distance | +| total-time | Number:Time | metrics#total-time | + +### Rain (if supported) + +| Channel | Type | ChannelName | +|---------|-------------|--------------| +| state | Switch | rain#state | +| counter | Number:Time | rain#counter | +| delay | Number:Time | rain#delay | + +### Wifi + +| Channel | Type | ChannelName | +|--------------|--------------|-------------------| +| rssi | Number:Power | wifi#rssi | +| wifi-quality | Number | wifi#wifi-quality | + +## Examples + +### $OPENHAB_CONF/items/landroid.things + +```java +Bridge worxlandroid:bridge:api "Worx Api" [ username="xxxxYYYxxxx", password="dldkssdjldj" ] { + Thing mower lanmower "Worx M600" [ serialNumber="sdmldksmdskmlsd" ] +} + +``` + + +### $OPENHAB_CONF/items/landroid.items + +```java +String MyMower "MyMower [%s]" +String LandroidMowerCommonStatus "Status code" {channel="worxlandroid:mower:MyWorxBridge:mymower:common#status"} +String LandroidMowerCommonError "Error code" {channel="worxlandroid:mower:MyWorxBridge:mymower:common#error"} +Switch LandroidMowerCommonOnline "Online" {channel="worxlandroid:mower:MyWorxBridge:mymower:common#online"} +DateTime LandroidMowerCommonOnlineTimestamp "Online status timestamp" {channel="worxlandroid:mower:MyWorxBridge:mymower:common#online-timestamp"} +String LandroidMowerCommonAction "Action" {channel="worxlandroid:mower:MyWorxBridge:mymower:common#action"} +Switch LandroidMowerCommonEnable "Mowing enabled" {channel="worxlandroid:mower:MyWorxBridge:mymower:common#enable"} +Switch LandroidMowerCommonLock "Lock mower wifi" {channel="worxlandroid:mower:MyWorxBridge:mymower:common#lock"} +DateTime LandroidMowerConfigTimestamp "Last update" {channel="worxlandroid:mower:MyWorxBridge:mymower:config#timestamp"} +Number LandroidMowerConfigCommand "Command" {channel="worxlandroid:mower:MyWorxBridge:mymower:config#command"} +Switch LandroidMowerMultiZonesEnable "Multizone enabled" {channel="worxlandroid:mower:MyWorxBridge:mymower:multi-zones#enable"} +Number LandroidMowerMultiZonesLastZone "Last zone" {channel="worxlandroid:mower:MyWorxBridge:mymower:multi-zones#last-zone"} +Number:Length LandroidMowerMultiZonesZone1 "Meters zone 1" {channel="worxlandroid:mower:MyWorxBridge:mymower:multi-zones#zone-1"} +Number:Length LandroidMowerMultiZonesZone2 "Meters zone 2" {channel="worxlandroid:mower:MyWorxBridge:mymower:multi-zones#zone-2"} +Number:Length LandroidMowerMultiZonesZone3 "Meters zone 3" {channel="worxlandroid:mower:MyWorxBridge:mymower:multi-zones#zone-3"} +Number:Length LandroidMowerMultiZonesZone4 "Meters zone 4" {channel="worxlandroid:mower:MyWorxBridge:mymower:multi-zones#zone-4"} +Number LandroidMowerMultiZonesAllocation0 "Zone allocation 1" {channel="worxlandroid:mower:MyWorxBridge:mymower:multi-zones#allocation-0"} +Number LandroidMowerMultiZonesAllocation1 "Zone allocation 2" {channel="worxlandroid:mower:MyWorxBridge:mymower:multi-zones#allocation-1"} +Number LandroidMowerMultiZonesAllocation2 "Zone allocation 3" {channel="worxlandroid:mower:MyWorxBridge:mymower:multi-zones#allocation-2"} +Number LandroidMowerMultiZonesAllocation3 "Zone allocation 4" {channel="worxlandroid:mower:MyWorxBridge:mymower:multi-zones#allocation-3"} +Number LandroidMowerMultiZonesAllocation4 "Zone allocation 5" {channel="worxlandroid:mower:MyWorxBridge:mymower:multi-zones#allocation-4"} +Number LandroidMowerMultiZonesAllocation5 "Zone allocation 6" {channel="worxlandroid:mower:MyWorxBridge:mymower:multi-zones#allocation-5"} +Number LandroidMowerMultiZonesAllocation6 "Zone allocation 7" {channel="worxlandroid:mower:MyWorxBridge:mymower:multi-zones#allocation-6"} +Number LandroidMowerMultiZonesAllocation7 "Zone allocation 8" {channel="worxlandroid:mower:MyWorxBridge:mymower:multi-zones#allocation-7"} +Number LandroidMowerMultiZonesAllocation8 "Zone allocation 9" {channel="worxlandroid:mower:MyWorxBridge:mymower:multi-zones#allocation-8"} +Number LandroidMowerMultiZonesAllocation9 "Zone allocation 10" {channel="worxlandroid:mower:MyWorxBridge:mymower:multi-zones#allocation-9"} +String LandroidMowerScheduleMode "Schedule mode" {channel="worxlandroid:mower:MyWorxBridge:mymower:schedule#mode"} +Number:Dimensionless LandroidMowerScheduleTimeExtension "Schedule time extension" {channel="worxlandroid:mower:MyWorxBridge:mymower:schedule#time-extension"} +DateTime LandroidMowerScheduleNextStart "Next start" {channel="worxlandroid:mower:MyWorxBridge:mymower:schedule#next-start"} +DateTime LandroidMowerScheduleNextStop "Next stop" {channel="worxlandroid:mower:MyWorxBridge:mymower:schedule#next-stop"} +Switch LandroidMowerAwsPoll "Poll AWS" {channel="worxlandroid:mower:MyWorxBridge:mymower:aws#poll"} +Switch LandroidMowerAwsConnected "AWS connected" {channel="worxlandroid:mower:MyWorxBridge:mymower:aws#connected"} +Switch LandroidMowerSundayEnable "Active" {channel="worxlandroid:mower:MyWorxBridge:mymower:sunday#enable"} +DateTime LandroidMowerSundayTime "Start time" {channel="worxlandroid:mower:MyWorxBridge:mymower:sunday#time"} +Number:Time LandroidMowerSundayDuration "Duration" {channel="worxlandroid:mower:MyWorxBridge:mymower:sunday#duration", unit="min"} +Switch LandroidMowerSundayEdgecut "Edgecut" {channel="worxlandroid:mower:MyWorxBridge:mymower:sunday#edgecut"} +Switch LandroidMowerSunday2Enable "Active" {channel="worxlandroid:mower:MyWorxBridge:mymower:sunday2#enable"} +DateTime LandroidMowerSunday2Time "Start time" {channel="worxlandroid:mower:MyWorxBridge:mymower:sunday2#time"} +Number:Time LandroidMowerSunday2Duration "Duration" {channel="worxlandroid:mower:MyWorxBridge:mymower:sunday2#duration", unit="min"} +Switch LandroidMowerSunday2Edgecut "Edgecut" {channel="worxlandroid:mower:MyWorxBridge:mymower:sunday2#edgecut"} +Switch LandroidMowerMondayEnable "Active" {channel="worxlandroid:mower:MyWorxBridge:mymower:monday#enable"} +DateTime LandroidMowerMondayTime "Start time" {channel="worxlandroid:mower:MyWorxBridge:mymower:monday#time"} +Number:Time LandroidMowerMondayDuration "Duration" {channel="worxlandroid:mower:MyWorxBridge:mymower:monday#duration", unit="min"} +Switch LandroidMowerMondayEdgecut "Edgecut" {channel="worxlandroid:mower:MyWorxBridge:mymower:monday#edgecut"} +Switch LandroidMowerMonday2Enable "Active" {channel="worxlandroid:mower:MyWorxBridge:mymower:monday2#enable"} +DateTime LandroidMowerMonday2Time "Start time" {channel="worxlandroid:mower:MyWorxBridge:mymower:monday2#time"} +Number:Time LandroidMowerMonday2Duration "Duration" {channel="worxlandroid:mower:MyWorxBridge:mymower:monday2#duration", unit="min"} +Switch LandroidMowerMonday2Edgecut "Edgecut" {channel="worxlandroid:mower:MyWorxBridge:mymower:monday2#edgecut"} +Switch LandroidMowerTuesdayEnable "Active" {channel="worxlandroid:mower:MyWorxBridge:mymower:tuesday#enable"} +DateTime LandroidMowerTuesdayTime "Start time" {channel="worxlandroid:mower:MyWorxBridge:mymower:tuesday#time"} +Number:Time LandroidMowerTuesdayDuration "Duration" {channel="worxlandroid:mower:MyWorxBridge:mymower:tuesday#duration", unit="min"} +Switch LandroidMowerTuesdayEdgecut "Edgecut" {channel="worxlandroid:mower:MyWorxBridge:mymower:tuesday#edgecut"} +Switch LandroidMowerTuesday2Enable "Active" {channel="worxlandroid:mower:MyWorxBridge:mymower:tuesday2#enable"} +DateTime LandroidMowerTuesday2Time "Start time" {channel="worxlandroid:mower:MyWorxBridge:mymower:tuesday2#time"} +Number:Time LandroidMowerTuesday2Duration "Duration" {channel="worxlandroid:mower:MyWorxBridge:mymower:tuesday2#duration", unit="min"} +Switch LandroidMowerTuesday2Edgecut "Edgecut" {channel="worxlandroid:mower:MyWorxBridge:mymower:tuesday2#edgecut"} +Switch LandroidMowerWednesdayEnable "Active" {channel="worxlandroid:mower:MyWorxBridge:mymower:wednesday#enable"} +DateTime LandroidMowerWednesdayTime "Start time" {channel="worxlandroid:mower:MyWorxBridge:mymower:wednesday#time"} +Number:Time LandroidMowerWednesdayDuration "Duration" {channel="worxlandroid:mower:MyWorxBridge:mymower:wednesday#duration", unit="min"} +Switch LandroidMowerWednesdayEdgecut "Edgecut" {channel="worxlandroid:mower:MyWorxBridge:mymower:wednesday#edgecut"} +Switch LandroidMowerWednesday2Enable "Active" {channel="worxlandroid:mower:MyWorxBridge:mymower:wednesday2#enable"} +DateTime LandroidMowerWednesday2Time "Start time" {channel="worxlandroid:mower:MyWorxBridge:mymower:wednesday2#time"} +Number:Time LandroidMowerWednesday2Duration "Duration" {channel="worxlandroid:mower:MyWorxBridge:mymower:wednesday2#duration", unit="min"} +Switch LandroidMowerWednesday2Edgecut "Edgecut" {channel="worxlandroid:mower:MyWorxBridge:mymower:wednesday2#edgecut"} +Switch LandroidMowerThursdayEnable "Active" {channel="worxlandroid:mower:MyWorxBridge:mymower:thursday#enable"} +DateTime LandroidMowerThursdayTime "Start time" {channel="worxlandroid:mower:MyWorxBridge:mymower:thursday#time"} +Number:Time LandroidMowerThursdayDuration "Duration" {channel="worxlandroid:mower:MyWorxBridge:mymower:thursday#duration", unit="min"} +Switch LandroidMowerThursdayEdgecut "Edgecut" {channel="worxlandroid:mower:MyWorxBridge:mymower:thursday#edgecut"} +Switch LandroidMowerThursday2Enable "Active" {channel="worxlandroid:mower:MyWorxBridge:mymower:thursday2#enable"} +DateTime LandroidMowerThursday2Time "Start time" {channel="worxlandroid:mower:MyWorxBridge:mymower:thursday2#time"} +Number:Time LandroidMowerThursday2Duration "Duration" {channel="worxlandroid:mower:MyWorxBridge:mymower:thursday2#duration", unit="min"} +Switch LandroidMowerThursday2Edgecut "Edgecut" {channel="worxlandroid:mower:MyWorxBridge:mymower:thursday2#edgecut"} +Switch LandroidMowerFridayEnable "Active" {channel="worxlandroid:mower:MyWorxBridge:mymower:friday#enable"} +DateTime LandroidMowerFridayTime "Start time" {channel="worxlandroid:mower:MyWorxBridge:mymower:friday#time"} +Number:Time LandroidMowerFridayDuration "Duration" {channel="worxlandroid:mower:MyWorxBridge:mymower:friday#duration", unit="min"} +Switch LandroidMowerFridayEdgecut "Edgecut" {channel="worxlandroid:mower:MyWorxBridge:mymower:friday#edgecut"} +Switch LandroidMowerFriday2Enable "Active" {channel="worxlandroid:mower:MyWorxBridge:mymower:friday2#enable"} +DateTime LandroidMowerFriday2Time "Start time" {channel="worxlandroid:mower:MyWorxBridge:mymower:friday2#time"} +Number:Time LandroidMowerFriday2Duration "Duration" {channel="worxlandroid:mower:MyWorxBridge:mymower:friday2#duration", unit="min"} +Switch LandroidMowerFriday2Edgecut "Edgecut" {channel="worxlandroid:mower:MyWorxBridge:mymower:friday2#edgecut"} +Switch LandroidMowerSaturdayEnable "Active" {channel="worxlandroid:mower:MyWorxBridge:mymower:saturday#enable"} +DateTime LandroidMowerSaturdayTime "Start time" {channel="worxlandroid:mower:MyWorxBridge:mymower:saturday#time"} +Number:Time LandroidMowerSaturdayDuration "Duration" {channel="worxlandroid:mower:MyWorxBridge:mymower:saturday#duration", unit="min"} +Switch LandroidMowerSaturdayEdgecut "Edgecut" {channel="worxlandroid:mower:MyWorxBridge:mymower:saturday#edgecut"} +Switch LandroidMowerSaturday2Enable "Active" {channel="worxlandroid:mower:MyWorxBridge:mymower:saturday2#enable"} +DateTime LandroidMowerSaturday2Time "Start time" {channel="worxlandroid:mower:MyWorxBridge:mymower:saturday2#time"} +Number:Time LandroidMowerSaturday2Duration "Duration" {channel="worxlandroid:mower:MyWorxBridge:mymower:saturday2#duration", unit="min"} +Switch LandroidMowerSaturday2Edgecut "Edgecut" {channel="worxlandroid:mower:MyWorxBridge:mymower:saturday2#edgecut"} +Switch LandroidMowerOneTimeEdgecut "Schedule edgecut" {channel="worxlandroid:mower:MyWorxBridge:mymower:one-time#edgecut"} +Number:Time LandroidMowerOneTimeDuration "Edgecut duration" {channel="worxlandroid:mower:MyWorxBridge:mymower:one-time#duration", unit="min"} +Number:Temperature LandroidMowerBatteryTemperature "Battery temperature" {channel="worxlandroid:mower:MyWorxBridge:mymower:battery#temperature"} +Number:ElectricPotential LandroidMowerBatteryVoltage "Battery voltage" {channel="worxlandroid:mower:MyWorxBridge:mymower:battery#voltage"} +Number LandroidMowerBatteryLevel "Battery level" {channel="worxlandroid:mower:MyWorxBridge:mymower:battery#level"} +Number LandroidMowerBatteryChargeCycles "Current charge cycles" {channel="worxlandroid:mower:MyWorxBridge:mymower:battery#charge-cycles"} +Number LandroidMowerBatteryChargeCyclesTotal "Total charge cycles" {channel="worxlandroid:mower:MyWorxBridge:mymower:battery#charge-cycles-total"} +Switch LandroidMowerBatteryCharging "Battery charging" {channel="worxlandroid:mower:MyWorxBridge:mymower:battery#charging"} +Number:Angle LandroidMowerOrientationPitch "Pitch" {channel="worxlandroid:mower:MyWorxBridge:mymower:orientation#pitch"} +Number:Angle LandroidMowerOrientationRoll "Roll" {channel="worxlandroid:mower:MyWorxBridge:mymower:orientation#roll"} +Number:Angle LandroidMowerOrientationYaw "Yaw" {channel="worxlandroid:mower:MyWorxBridge:mymower:orientation#yaw"} +Number:Time LandroidMowerMetricsBladeTime "Current blade time" {channel="worxlandroid:mower:MyWorxBridge:mymower:metrics#blade-time", unit="h"} +Number:Time LandroidMowerMetricsBladeTimeTotal "Total blade time" {channel="worxlandroid:mower:MyWorxBridge:mymower:metrics#blade-time-total", unit="h"} +Number:Length LandroidMowerMetricsDistance "Total distance" {channel="worxlandroid:mower:MyWorxBridge:mymower:metrics#distance", unit="km"} +Number:Time LandroidMowerMetricsTotalTime "Total time" {channel="worxlandroid:mower:MyWorxBridge:mymower:metrics#total-time", unit="h"} +Switch LandroidMowerRainState "State" {channel="worxlandroid:mower:MyWorxBridge:mymower:rain#state"} +Number:Time LandroidMowerRainCounter "Counter" {channel="worxlandroid:mower:MyWorxBridge:mymower:rain#counter", unit="min"} +Number:Time LandroidMowerRainDelay "Delay" {channel="worxlandroid:mower:MyWorxBridge:mymower:rain#delay", unit="min"} +Number:Power LandroidMowerWifiRssi "Rssi" {channel="worxlandroid:mower:MyWorxBridge:mymower:wifi#rssi", unit="dBm"} +Number LandroidMowerWifiWifiQuality "Wifi quality" {channel="worxlandroid:mower:MyWorxBridge:mymower:wifi#wifi-quality"} + +``` + +### $OPENHAB_CONF/sitemaps/landroid.sitemap + +```perl +sitemap landroid label="Landroid" +{ + Frame label="Worx Landroid Mower" { + Text label="Status" item=MyMower icon=none { + Default item=LandroidMowerCommonStatus + Default item=LandroidMowerCommonError + Default item=LandroidMowerCommonOnline + Default item=LandroidMowerCommonOnlineTimestamp + Default item=LandroidMowerConfigTimestamp + Default item=LandroidMowerConfigCommand + Default item=LandroidMowerAwsPoll + Text item=LandroidMowerAwsConnected label="AWS connected [%s]" + Default item=LandroidMowerOrientationPitch + Default item=LandroidMowerOrientationRoll + Default item=LandroidMowerOrientationYaw + Default item=LandroidMowerMetricsBladeTime + Default item=LandroidMowerMetricsBladeTimeTotal + Default item=LandroidMowerMetricsDistance + Default item=LandroidMowerMetricsTotalTime + Default item=LandroidMowerWifiRssi + Default item=LandroidMowerWifiWifiQuality + } + Text label="Control" icon=none { + Default item=LandroidMowerCommonAction + Default item=LandroidMowerCommonEnable + Default item=LandroidMowerCommonLock + Default item=LandroidMowerOneTimeEdgecut + Setpoint item=LandroidMowerOneTimeDuration minValue=30 maxValue=300 step=30 + } + Text label="Multi zones" icon=none { + Default item=LandroidMowerMultiZonesEnable + Default item=LandroidMowerMultiZonesLastZone + Default item=LandroidMowerMultiZonesZone1 + Default item=LandroidMowerMultiZonesZone2 + Default item=LandroidMowerMultiZonesZone3 + Default item=LandroidMowerMultiZonesZone4 + Default item=LandroidMowerMultiZonesAllocation0 + Default item=LandroidMowerMultiZonesAllocation1 + Default item=LandroidMowerMultiZonesAllocation2 + Default item=LandroidMowerMultiZonesAllocation3 + Default item=LandroidMowerMultiZonesAllocation4 + Default item=LandroidMowerMultiZonesAllocation5 + Default item=LandroidMowerMultiZonesAllocation6 + Default item=LandroidMowerMultiZonesAllocation7 + Default item=LandroidMowerMultiZonesAllocation8 + Default item=LandroidMowerMultiZonesAllocation9 + } + Text label="Schedule" icon=none { + Default item=LandroidMowerScheduleMode + Setpoint item=LandroidMowerScheduleTimeExtension minValue=-100 maxValue=100 step=10 + Default item=LandroidMowerScheduleNextStart + Default item=LandroidMowerScheduleNextStop + Text label="Sunday" icon=none { + Default item=LandroidMowerSundayEnable + Default item=LandroidMowerSundayTime + Default item=LandroidMowerSundayDuration + Default item=LandroidMowerSundayEdgecut + Default item=LandroidMowerSunday2Enable + Default item=LandroidMowerSunday2Time + Default item=LandroidMowerSunday2Duration + Default item=LandroidMowerSunday2Edgecut + } + Text label="Monday" icon=none { + Default item=LandroidMowerMondayEnable + Default item=LandroidMowerMondayTime + Default item=LandroidMowerMondayDuration + Default item=LandroidMowerMondayEdgecut + Default item=LandroidMowerMonday2Enable + Default item=LandroidMowerMonday2Time + Default item=LandroidMowerMonday2Duration + Default item=LandroidMowerMonday2Edgecut + } + Text label="Tuesday" icon=none { + Default item=LandroidMowerTuesdayEnable + Default item=LandroidMowerTuesdayTime + Default item=LandroidMowerTuesdayDuration + Default item=LandroidMowerTuesdayEdgecut + Default item=LandroidMowerTuesday2Enable + Default item=LandroidMowerTuesday2Time + Default item=LandroidMowerTuesday2Duration + Default item=LandroidMowerTuesday2Edgecut + } + Text label="Wednesday" icon=none { + Default item=LandroidMowerWednesdayEnable + Default item=LandroidMowerWednesdayTime + Default item=LandroidMowerWednesdayDuration + Default item=LandroidMowerWednesdayEdgecut + Default item=LandroidMowerWednesday2Enable + Default item=LandroidMowerWednesday2Time + Default item=LandroidMowerWednesday2Duration + Default item=LandroidMowerWednesday2Edgecut + } + Text label="Thursday" icon=none { + Default item=LandroidMowerThursdayEnable + Default item=LandroidMowerThursdayTime + Default item=LandroidMowerThursdayDuration + Default item=LandroidMowerThursdayEdgecut + Default item=LandroidMowerThursday2Enable + Default item=LandroidMowerThursday2Time + Default item=LandroidMowerThursday2Duration + Default item=LandroidMowerThursday2Edgecut + } + Text label="Friday" icon=none { + Default item=LandroidMowerFridayEnable + Default item=LandroidMowerFridayTime + Default item=LandroidMowerFridayDuration + Default item=LandroidMowerFridayEdgecut + Default item=LandroidMowerFriday2Enable + Default item=LandroidMowerFriday2Time + Default item=LandroidMowerFriday2Duration + Default item=LandroidMowerFriday2Edgecut + } + Text label="Saturday" icon=none { + Default item=LandroidMowerSaturdayEnable + Default item=LandroidMowerSaturdayTime + Default item=LandroidMowerSaturdayDuration + Default item=LandroidMowerSaturdayEdgecut + Default item=LandroidMowerSaturday2Enable + Default item=LandroidMowerSaturday2Time + Default item=LandroidMowerSaturday2Duration + Default item=LandroidMowerSaturday2Edgecut + } + } + Text label="Battery" icon=none { + Default item=LandroidMowerBatteryTemperature + Default item=LandroidMowerBatteryVoltage + Default item=LandroidMowerBatteryLevel + Default item=LandroidMowerBatteryChargeCycles + Default item=LandroidMowerBatteryChargeCyclesTotal + Text item=LandroidMowerBatteryCharging label="Battery charging [%s]" + } + Text label="Rainsensor" icon=none { + Text item=LandroidMowerRainState label="State [%s]" + Default item=LandroidMowerRainCounter + Setpoint item=LandroidMowerRainDelay minValue=30 maxValue=600 step=15 + } + } +} + +``` + +### $OPENHAB_CONF/rules/landroid.rules + +```java + +rule "Landroid mower status" +when + Item LandroidMowerCommonError changed or + Item LandroidMowerCommonStatus changed +then + if (LandroidMowerCommonError.state != "NO_ERR") { + MyMower.postUpdate(transform("MAP", "landroid_error_de.map", LandroidMowerCommonError.state.toString)) + } else { + MyMower.postUpdate(transform("MAP", "landroid_status_de.map", LandroidMowerCommonStatus.state.toString)) + } +end + +``` + +### $OPENHAB_CONF/transform/landroid_error_en.map + +```text + +UNKNOWN=unknown +NO_ERR=no error +TRAPPED=trapped +LIFTED=lifted +WIRE_MISSING=wire missing +OUTSIDE_WIRE=outside wire +RAINING=raining +CLOSE_DOOR_TO_MOW=close door to mow +CLOSE_DOOR_TO_GO_HOME=close door to go home +BLADE_MOTOR_BLOCKED=blade motor blocked +WHEEL_MOTOR_BLOKED=wheel motor blocked +TRAPPED_TIMEOUT=trapped timeout +UPSIDE_DOWN=upside down +BATTERY_LOW=battery low +REVERSE_WIRE=reverse wire +CHARGE_ERROR=charge error +TIMEOUT_FINDING_HOME=timeout finding home +MOWER_LOCKED=mower locked +BATTERY_OVER_TEMPERATURE=battery over temperature +MOWER_OUTSIDE_WIRE=mower outside wire + +``` + +### $OPENHAB_CONF/transform/landroid_status_en.map + +```text + +UNKNOWN=unknown +IDLE=idle +HOME=home +START_SEQUENCE=start sequence +LEAVING_HOME=leaving home +FOLLOW_WIRE=follow wire +SEARCHING_HOME=searching home +SEARCHING_WIRE=searching wire +MOWING=mowing +LIFTED=lifted +TRAPPED=trapped +BLADE_BLOCKED=blade blocked +DEBUG=debug +REMOTE_CONTROL=remote control +GOING_HOME=going home +ZONE_TRAINING=zone training +BORDER_CUT=border cut +SEARCHING_ZONE=searching zone +PAUSE=pause +MANUEL_STOP=manuel stop + +``` diff --git a/bundles/org.openhab.binding.worxlandroid/images/landroid.svg b/bundles/org.openhab.binding.worxlandroid/images/landroid.svg new file mode 100644 index 0000000000000..e7af19e961ae9 --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/images/landroid.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.worxlandroid/pom.xml b/bundles/org.openhab.binding.worxlandroid/pom.xml new file mode 100644 index 0000000000000..b36395c7171a6 --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/pom.xml @@ -0,0 +1,32 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 5.1.0-SNAPSHOT + + + org.openhab.binding.worxlandroid + + openHAB Add-ons :: Bundles :: Worx Landroid Binding + + + + software.amazon.awssdk.iotdevicesdk + aws-iot-device-sdk + 1.27.4 + provided + + + software.amazon.awssdk.crt + aws-crt + 0.38.13 + provided + + + + diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/feature/feature.xml b/bundles/org.openhab.binding.worxlandroid/src/main/feature/feature.xml new file mode 100644 index 0000000000000..0b3f5adcfcd6f --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/feature/feature.xml @@ -0,0 +1,10 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.worxlandroid/${project.version} + mvn:org.openhab.osgiify/software.amazon.awssdk.iotdevicesdk.aws-iot-device-sdk/1.27.4 + + diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/WorxLandroidBindingConstants.java b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/WorxLandroidBindingConstants.java new file mode 100644 index 0000000000000..5bcd1fea13ac7 --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/WorxLandroidBindingConstants.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.worxlandroid.internal; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link WorxLandroidBindingConstants} class defines datCommon constants, which are + * used across the whole binding. + * + * @author Nils Billing - Initial contribution + */ +@NonNullByDefault +public class WorxLandroidBindingConstants { + + public static final String BINDING_ID = "worxlandroid"; + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "bridge"); + public static final ThingTypeUID THING_TYPE_MOWER = new ThingTypeUID(BINDING_ID, "mower"); + + public static final Set SUPPORTED_THING_TYPES = Set.of(THING_TYPE_MOWER); + + // Channel group ids + public static final String GROUP_COMMON = "common"; + public static final String GROUP_CONFIG = "config"; + public static final String GROUP_MULTI_ZONES = "multi-zones"; + public static final String GROUP_SCHEDULE = "schedule"; + public static final String GROUP_ONE_TIME = "one-time"; + public static final String GROUP_BATTERY = "battery"; + public static final String GROUP_ORIENTATION = "orientation"; + public static final String GROUP_METRICS = "metrics"; + public static final String GROUP_RAIN = "rain"; + public static final String GROUP_WIFI = "wifi"; + public static final String GROUP_AWS = "aws"; + + // List channel ids + // common + public static final String CHANNEL_ONLINE_TIMESTAMP = "online-timestamp"; + public static final String CHANNEL_ACTION = "action"; + public static final String CHANNEL_ENABLE = "enable"; + public static final String CHANNEL_ONLINE = "online"; + public static final String CHANNEL_LOCK = "lock"; + public static final String CHANNEL_RSSI = "rssi"; + + // AWS + public static final String CHANNEL_POLL = "poll"; + public static final String CHANNEL_CONNECTED = "connected"; + + // cfgCommon + public static final String CHANNEL_TIMESTAMP = "timestamp"; + public static final String CHANNEL_COMMAND = "command"; + public static final String CHANNEL_DELAY = "delay"; + + // cfgSc + public static final String CHANNEL_TIME_EXTENSION = "time-extension"; + public static final String CHANNEL_MODE = "mode"; + public static final String CHANNEL_START = "next-start"; + public static final String CHANNEL_STOP = "next-stop"; + + // cfgScXXXday + public static final String CHANNEL_DURATION = "duration"; + public static final String CHANNEL_EDGECUT = "edgecut"; + public static final String CHANNEL_TIME = "time"; + + // datCommon + public static final String CHANNEL_WIFI_QUALITY = "wifi-quality"; + public static final String CHANNEL_LAST_ZONE = "last-zone"; + public static final String CHANNEL_STATUS_CODE = "status"; + public static final String CHANNEL_ERROR_CODE = "error"; + + // datBattery + public static final String CHANNEL_TEMPERATURE = "temperature"; + public static final String CHANNEL_VOLTAGE = "voltage"; + public static final String CHANNEL_LEVEL = "level"; + public static final String CHANNEL_CHARGE_CYCLES = "charge-cycles"; + public static final String CHANNEL_CHARGE_CYCLES_TOTAL = "charge-cycles-total"; + public static final String CHANNEL_CHARGING = "charging"; + + // datDmp + public static final String CHANNEL_PITCH = "pitch"; + public static final String CHANNEL_ROLL = "roll"; + public static final String CHANNEL_YAW = "yaw"; + + // datSt + public static final String CHANNEL_BLADE_TIME = "blade-time"; + public static final String CHANNEL_BLADE_TIME_TOTAL = "blade-time-total"; + public static final String CHANNEL_DISTANCE = "distance"; + public static final String CHANNEL_TOTAL_TIME = "total-time"; + + // datRain + public static final String CHANNEL_RAIN_STATE = "state"; + public static final String CHANNEL_RAIN_COUNTER = "counter"; + + public static final String CHANNEL_PREFIX_ALLOCATION = "allocation-%d"; + public static final String CHANNEL_PREFIX_ZONE = "zone-%d"; +} diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/WorxLandroidHandlerFactory.java b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/WorxLandroidHandlerFactory.java new file mode 100644 index 0000000000000..7e75f1bf3c654 --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/WorxLandroidHandlerFactory.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.worxlandroid.internal; + +import static org.openhab.binding.worxlandroid.internal.WorxLandroidBindingConstants.*; + +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.worxlandroid.internal.api.WorxApiHandler; +import org.openhab.binding.worxlandroid.internal.discovery.MowerDiscoveryService; +import org.openhab.binding.worxlandroid.internal.handler.WorxLandroidBridgeHandler; +import org.openhab.binding.worxlandroid.internal.handler.WorxLandroidMowerHandler; +import org.openhab.core.auth.client.oauth2.OAuthFactory; +import org.openhab.core.config.discovery.DiscoveryService; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link WorxLandroidHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Nils Billing - Initial contribution + * @author Gaël L'hopital - Added oAuthFactory + */ +@NonNullByDefault +@Component(configurationPid = "binding.worxlandroid", service = ThingHandlerFactory.class) +public class WorxLandroidHandlerFactory extends BaseThingHandlerFactory { + private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_MOWER, THING_TYPE_BRIDGE); + + private final Map> discoveryServiceRegs = new HashMap<>(); + private final OAuthFactory oAuthFactory; + private final WorxApiHandler worxApiHandler; + + @Activate + public WorxLandroidHandlerFactory(final @Reference OAuthFactory oAuthFactory, + final @Reference WorxApiHandler worxApiHandler) { + this.oAuthFactory = oAuthFactory; + this.worxApiHandler = worxApiHandler; + } + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (THING_TYPE_BRIDGE.equals(thingTypeUID)) { + WorxLandroidBridgeHandler bridgeHandler = new WorxLandroidBridgeHandler((Bridge) thing, worxApiHandler, + oAuthFactory); + MowerDiscoveryService discoveryService = new MowerDiscoveryService(bridgeHandler); + discoveryServiceRegs.put(thing.getUID(), bundleContext.registerService(DiscoveryService.class.getName(), + discoveryService, new Hashtable<>())); + + return bridgeHandler; + } else if (THING_TYPE_MOWER.equals(thingTypeUID)) { + return new WorxLandroidMowerHandler(thing, worxApiHandler.getDeserializer()); + } + return null; + } + + @Override + protected void removeHandler(ThingHandler handler) { + if (handler instanceof WorxLandroidBridgeHandler) { + ServiceRegistration serviceReg = discoveryServiceRegs.remove(handler.getThing().getUID()); + if (serviceReg != null) { + serviceReg.unregister(); + } + } + super.removeHandler(handler); + } +} diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/WorxLandroidIconProvider.java b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/WorxLandroidIconProvider.java new file mode 100644 index 0000000000000..41f40f8702b6f --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/WorxLandroidIconProvider.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.worxlandroid.internal; + +import static org.openhab.binding.worxlandroid.internal.WorxLandroidBindingConstants.BINDING_ID; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Locale; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.i18n.TranslationProvider; +import org.openhab.core.ui.icon.IconProvider; +import org.openhab.core.ui.icon.IconSet; +import org.openhab.core.ui.icon.IconSet.Format; +import org.osgi.framework.BundleContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link WorxLandroidIconProvider} is the class providing binding related icons. + * + * @author Gaël L'hopital - Initial contribution + */ +@Component(service = { IconProvider.class, WorxLandroidIconProvider.class }) +@NonNullByDefault +public class WorxLandroidIconProvider implements IconProvider { + private static final String DEFAULT_LABEL = "Worx Landroid Icons"; + private static final String DEFAULT_DESCRIPTION = "Icons illustrating channels provided by Worx Landroid binding."; + + private final Logger logger = LoggerFactory.getLogger(WorxLandroidIconProvider.class); + private final BundleContext context; + private final TranslationProvider i18nProvider; + + @Activate + public WorxLandroidIconProvider(final BundleContext context, final @Reference TranslationProvider i18nProvider) { + this.context = context; + this.i18nProvider = i18nProvider; + } + + @Override + public Set getIconSets() { + return getIconSets(null); + } + + @Override + public Set getIconSets(@Nullable Locale locale) { + String label = getText("label", DEFAULT_LABEL, locale); + String description = getText("decription", DEFAULT_DESCRIPTION, locale); + + return Set.of(new IconSet(BINDING_ID, label, description, Set.of(Format.SVG))); + } + + private String getText(String entry, String defaultValue, @Nullable Locale locale) { + String text = defaultValue; + if (locale != null) { + text = i18nProvider.getText(context.getBundle(), "iconset." + entry, defaultValue, locale); + text = text == null ? defaultValue : text; + } + return text; + } + + @Override + public @Nullable Integer hasIcon(String category, String iconSetId, Format format) { + return iconSetId.equals(BINDING_ID) && format == Format.SVG ? 0 : null; + } + + public @Nullable InputStream getIcon(String category, String state) { + return getIcon(category, BINDING_ID, state, Format.SVG); + } + + @Override + public @Nullable InputStream getIcon(String category, String iconSetId, @Nullable String state, Format format) { + String icon = getResource(category, true); + if (icon.isEmpty()) { + return null; + } + + if (state != null) { + String withState = "%s-%s".formatted(category, state.toString().toLowerCase()); + String iconWithState = getResource(withState, false); + if (!iconWithState.isEmpty()) { + icon = iconWithState; + } + } + + return new ByteArrayInputStream(icon.getBytes()); + } + + private String getResource(String iconName, boolean shouldExist) { + String result = ""; + + URL iconResource = context.getBundle().getEntry("icon/%s.svg".formatted(iconName)); + if (iconResource != null) { + try (InputStream stream = iconResource.openStream()) { + result = new String(stream.readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException e) { + logger.warn("Unable to load resource '{}': {}", iconResource.getPath(), e.getMessage()); + } + } else if (shouldExist) { + logger.warn("Unable to find icon '{}'", iconName); + } + return result; + } +} diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/api/WebApiException.java b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/api/WebApiException.java new file mode 100644 index 0000000000000..eda61ede762cd --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/api/WebApiException.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.worxlandroid.internal.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link WebApiException} is a class for handling the Worx Landroid API exceptions + * + * @author Nils Billing - Initial contribution + */ +@NonNullByDefault +public class WebApiException extends Exception { + private static final long serialVersionUID = 1L; + private static final int UNKNOWN = 0; + + private final int errorCode; + + public WebApiException(int errorCode, String errorMsg) { + super(errorMsg); + this.errorCode = errorCode; + } + + public WebApiException(String errorMsg, Throwable cause) { + super(errorMsg); + this.errorCode = UNKNOWN; + } + + public WebApiException(String errorMsg) { + super(errorMsg); + this.errorCode = UNKNOWN; + } + + public WebApiException(Throwable cause) { + super(cause.getMessage(), cause); + this.errorCode = UNKNOWN; + } + + public int getErrorCode() { + return errorCode; + } +} diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/api/WorxApiDeserializer.java b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/api/WorxApiDeserializer.java new file mode 100644 index 0000000000000..4557e317f5124 --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/api/WorxApiDeserializer.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.worxlandroid.internal.api; + +import java.lang.reflect.Type; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.i18n.TimeZoneProvider; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import com.google.gson.FieldNamingPolicy; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonSyntaxException; +import com.google.gson.reflect.TypeToken; + +/** + * The {@link WorxApiDeserializer} is responsible to instantiate suitable Gson (de)serializer + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +@Component(service = WorxApiDeserializer.class) +public class WorxApiDeserializer { + private static final DateTimeFormatter WORX_FORMATTER = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ssX"); + + private final Gson gson; + + @Activate + public WorxApiDeserializer(@Reference TimeZoneProvider timeZoneProvider) { + gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) + .registerTypeAdapter(ZoneId.class, + (JsonDeserializer) (json, type, context) -> ZoneId + .of(json.getAsJsonPrimitive().getAsString())) + .registerTypeAdapter(ZonedDateTime.class, + (JsonDeserializer) (json, type, context) -> ZonedDateTime + .parse(json.getAsJsonPrimitive().getAsString() + "Z", WORX_FORMATTER) + .withZoneSameInstant(timeZoneProvider.getTimeZone())) + .registerTypeAdapter(Boolean.class, (JsonDeserializer) (json, type, context) -> { + String value = json.getAsJsonPrimitive().getAsString().toUpperCase(); + return "1".equals(value); + }).create(); + } + + public String toJson(Object object) { + return gson.toJson(object); + } + + public Map toMap(Object object) { + Map fromObject = gson.fromJson(toJson(object), new TypeToken>() { + }.getType()); + return fromObject != null ? Map.copyOf(fromObject) : Map.of(); + } + + public T deserialize(Type typeToken, String json) throws WebApiException { + try { + @Nullable + T result = gson.fromJson(json, typeToken); + if (result != null) { + return result; + } + throw new WebApiException("Deserialization of '%s' resulted in null value".formatted(json)); + } catch (JsonSyntaxException e) { + throw new WebApiException("Unexpected error deserializing '%s' : %s".formatted(json, e.getMessage())); + } + } +} diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/api/WorxApiHandler.java b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/api/WorxApiHandler.java new file mode 100644 index 0000000000000..c7f9c4b225683 --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/api/WorxApiHandler.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.worxlandroid.internal.api; + +import java.lang.reflect.Type; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpMethod; +import org.openhab.binding.worxlandroid.internal.api.dto.ProductItemStatus; +import org.openhab.binding.worxlandroid.internal.api.dto.UsersMeResponse; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.reflect.TypeToken; + +/** + * {@link WorxApiHandler} is a API request + * + * @author Nils Billing - Initial contribution + */ +@NonNullByDefault +@Component(service = WorxApiHandler.class) +public class WorxApiHandler { + private static final String URL_BASE = "https://api.worxlandroid.com/api/v2/"; + private static final String URL_PRODUCT_ITEMS = URL_BASE + "product-items"; + private static final String URL_USERS_ME = URL_BASE + "users/me"; + + private static final Type PRODUCT_ITEM_STATUS_LIST = new TypeToken>() { + }.getType(); + private static final Type PRODUCT_ITEM_STATUS = new TypeToken() { + }.getType(); + private static final Type USERS_ME = new TypeToken() { + }.getType(); + + private final Logger logger = LoggerFactory.getLogger(WorxApiHandler.class); + private final HttpClient httpClient; + private final WorxApiDeserializer deserializer; + + @Activate + public WorxApiHandler(final @Reference HttpClientFactory httpClientFactory, + final @Reference WorxApiDeserializer deserializer) { + this.httpClient = httpClientFactory.getCommonHttpClient(); + this.deserializer = deserializer; + } + + private Request buildRequest(String url, String accessToken, HttpMethod method) { + Request request = httpClient.newRequest(url).method(method); + request.header(HttpHeader.AUTHORIZATION, "Bearer %s".formatted(accessToken)); + request.header(HttpHeader.CONTENT_TYPE, "application/json; utf-8"); + request.timeout(15, TimeUnit.SECONDS); + return request; + } + + private T apiGet(String url, String accessToken, Type type) throws WebApiException { + Request request = buildRequest(url, accessToken, HttpMethod.GET); + + logger.debug("URI: {}", request.getURI().toString()); + try { + ContentResponse response = request.send(); + if (response.getStatus() == 200) { + String result = response.getContentAsString(); + logger.trace("Worx Landroid Api Response: {}", result); + return deserializer.deserialize(type, result); + } + throw new WebApiException( + "Error calling Worx Landroid Api! HTTP Status = %d".formatted(response.getStatus())); + } catch (InterruptedException | TimeoutException | ExecutionException e) { + throw new WebApiException(e); + } + } + + private boolean apiPost(String url, String accessToken) { + Request request = buildRequest(url, accessToken, HttpMethod.POST); + + logger.debug("URI: {}", request.getURI().toString()); + try { + return request.send().getStatus() == 200; + } catch (InterruptedException | TimeoutException | ExecutionException e) { + logger.error("Error posting at {}: {}", request.getURI().toString(), e.getMessage()); + } + return false; + } + + public WorxApiDeserializer getDeserializer() { + return deserializer; + } + + public List retrieveDeviceStatus(String token) throws WebApiException { + return apiGet("%s?status=1".formatted(URL_PRODUCT_ITEMS), token, PRODUCT_ITEM_STATUS_LIST); + } + + public ProductItemStatus retrieveDeviceStatus(String token, String serialNumber) throws WebApiException { + return apiGet("%s/%s?status=1".formatted(URL_PRODUCT_ITEMS, serialNumber), token, PRODUCT_ITEM_STATUS); + } + + public UsersMeResponse retrieveMe(String token) throws WebApiException { + return apiGet(URL_USERS_ME, token, USERS_ME); + } + + public boolean resetBladeTime(String token, String serialNumber) { + return apiPost("%s/%s/counters/blade/reset".formatted(URL_PRODUCT_ITEMS, serialNumber), token); + } + + public boolean resetBatteryCycles(String token, String serialNumber) { + return apiPost("%s/%s/counters/battery/reset".formatted(URL_PRODUCT_ITEMS, serialNumber), token); + } +} diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/api/dto/Commands.java b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/api/dto/Commands.java new file mode 100644 index 0000000000000..7a826565ba60c --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/api/dto/Commands.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.worxlandroid.internal.api.dto; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.worxlandroid.internal.codes.WorxLandroidActionCodes; + +/** + * The {@link Commands} class hold record definition of Commands send to API + * + * @author Gaël L'hopital - Initial contribution + * + */ +@NonNullByDefault +public class Commands { + private record OTS(OTSCommand ots) { + } + + private record OTSCommand( // + int bc, // bordercut + int wtm // work time minutes + ) { + } + + public record OneTimeCommand(OTS sc) { + public OneTimeCommand(int bc, int wtm) { + this(new OTS(new OTSCommand(bc, wtm))); + } + } + + private record ScheduleDaysP(int p, Object d, @Nullable Object dd) { + } + + public record ScheduleDaysCommand(ScheduleDaysP sc) { + public ScheduleDaysCommand(int p, Object[] d, Object[] dd) { + this(new ScheduleDaysP(p, d, dd)); + } + + public ScheduleDaysCommand(int p, Object[] d) { + this(new ScheduleDaysP(p, d, null)); + } + } + + private record ScheduleCommandMode(int m) { + } + + public record ScheduleCommand(ScheduleCommandMode sc) { + public ScheduleCommand(int m) { + this(new ScheduleCommandMode(m)); + } + } + + public record MowerCommand(int cmd) { + public MowerCommand(WorxLandroidActionCodes actionCode) { + this(actionCode.code); + } + } + + public record ZoneMeterCommand(int[] mz) { + } + + public record ZoneMeterAlloc(int[] mzv) { + } + + public record SetRainDelay(int rd) { + } +} diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/api/dto/LastStatus.java b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/api/dto/LastStatus.java new file mode 100644 index 0000000000000..116fcb3864c9f --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/api/dto/LastStatus.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.worxlandroid.internal.api.dto; + +import java.time.ZonedDateTime; + +/** + * + * @author Gaël L'hopital - Initial contribution + */ +public class LastStatus { + public ZonedDateTime timestamp; + public Payload payload; + + public LastStatus(Payload payload) { + this.payload = payload; + this.timestamp = ZonedDateTime.now(); + } +} diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/api/dto/Payload.java b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/api/dto/Payload.java new file mode 100644 index 0000000000000..db2610ee390d1 --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/api/dto/Payload.java @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.worxlandroid.internal.api.dto; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; + +import org.openhab.binding.worxlandroid.internal.codes.WorxLandroidErrorCodes; +import org.openhab.binding.worxlandroid.internal.codes.WorxLandroidStatusCodes; + +import com.google.gson.annotations.SerializedName; + +/** + * + * @author Gaël L'hopital - Initial contribution + */ +public class Payload { + public class US { + public int enabled; + public String stat; + } + + public class Ots { + @SerializedName("wtm") + public int duration = -1; + private int bc = -1; + + public boolean getEdgeCut() { + return bc == 1; + } + } + + public class Al { + public int lvl; + public int t; + } + + public class Modules { + @SerializedName("US") + public US uS; + } + + public class Rain { + @SerializedName("s") + public Boolean raining; + @SerializedName("cnt") + public int counter = -1; + } + + public class Schedule { + public static enum Mode { + @SerializedName("1") + NORMAL, + @SerializedName("2") + PARTY, + UNKNOWN + } + + @SerializedName("m") + public Mode scheduleMode = Mode.UNKNOWN; + @SerializedName("p") + public int timeExtension = -1; + public int distm; + public Ots ots; + public List> d; + public List> dd; + } + + public class Battery { + @SerializedName("t") + public double temp = -1; + @SerializedName("v") + public double voltage = -1; + @SerializedName("p") + public int level = -1; + @SerializedName("nr") + public int chargeCycle = -1; + @SerializedName("c") + public Boolean charging; + public int m; + } + + public class Stat { + @SerializedName("b") + public int bladeWorkTime = -1; + @SerializedName("d") + public int distanceCovered = -1; + @SerializedName("wt") + public int mowerWorkTime = -1; + @SerializedName("bl") + public int lawnPerimeter; + } + + public class Cfg { + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss"); + + private String dt = ""; // "dt": "13/03/2020", + private String tm = ""; // "tm": "17:09:34" + + public int id = -1; + public String lg = ""; // en, fr... + public int cmd = -1; + public Schedule sc; + @SerializedName("mz") + public List multiZones = List.of(); + @SerializedName("mzv") + public List multizoneAllocations = List.of(); + @SerializedName("rd") + public int rainDelay = -1; + @SerializedName("sn") + public String serialNumber = ""; + public int mzk; + public Al al; + public int tq; + public Modules modules; + + public ZonedDateTime getDateTime(ZoneId zoneId) { + return dt.isEmpty() || tm.isEmpty() ? null + : ZonedDateTime.of(LocalDateTime.parse("%s %s".formatted(dt, tm), FORMATTER), zoneId); + } + } + + public class Dat { + public static enum Axis { + // Don't change order - ordinal is used + PITCH, + ROLL, + YAW; + } + + private int lk = -1; + @SerializedName("dmp") + private double[] dataMotionProcessor = { -1, -1, -1 }; // pitch, roll, yaw + + public String mac = ""; + public String fw = ""; + @SerializedName("bt") + public Battery battery; + public Stat st; + @SerializedName("ls") + public WorxLandroidStatusCodes statusCode = WorxLandroidStatusCodes.UNKNOWN; + @SerializedName("le") + public WorxLandroidErrorCodes errorCode = WorxLandroidErrorCodes.UNKNOWN; + @SerializedName("lz") + public int lastZone = -1; + @SerializedName("rsi") + public int wifiQuality; + public int fwb; + public String conn; + public int act; + public int tr; + public Rain rain; + public Modules modules; + + public boolean isLocked() { + return lk == 1; + } + + public double getAngle(Axis axis) { + return dataMotionProcessor[axis.ordinal()]; + } + } + + public Cfg cfg; + public Dat dat; +} diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/api/dto/ProductItemStatus.java b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/api/dto/ProductItemStatus.java new file mode 100644 index 0000000000000..c7185baa6471f --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/api/dto/ProductItemStatus.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.worxlandroid.internal.api.dto; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; + +/** + * The {@link ProductItemStatus} class + * + * @author Gaël L'hopital - Initial contribution + * + */ +public class ProductItemStatus { + + public class Accessories { + public boolean ultrasonic; + } + + public class MqttTopics { + public String commandIn; + public String commandOut; + } + + public class SetupLocation { + public double latitude; + public double longitude; + } + + public class AppSettings { + boolean cellularSetupCompleted; + } + + public class City { + public int id; + public int countryId; + public String name; + public double latitude; + public double longitude; + public String createdAt; + public String updatedAt; + } + + public class Sim { + public int id; + public String iccid; + public String simStatus; + public boolean pendingActivation; + public ZonedDateTime contractStartsAt; + public ZonedDateTime contractEndsAt; + public ZonedDateTime createdAt; + public ZonedDateTime updatedAt; + } + + public class AutoSchedule { + public int boost; + public String grassType; + public boolean irrigation; + public Map nutrition; + public String soilType; + } + + public String id; + public String uuid; + public int productId; + public String userId; + public String serialNumber; + public String macAddress; + public String name; + public boolean locked; + public String firmwareVersion; + public boolean firmwareAutoUpgrade; + public boolean pushNotifications; + public Sim sim; + public String pushNotificationsLevel; + public boolean test; + public boolean iotRegistered; + public boolean mqttRegistered; + public String pinCode; + public String registeredAt; + public boolean online; + public String mqttEndpoint; + public AppSettings appSettings; + public int protocol; + public String pendingRadioLinkValidation; + public List capabilities; + public List capabilitiesAvailable; + public Accessories accessories; + public MqttTopics mqttTopics; + public boolean warrantyRegistered; + public String purchasedAt; + public String warrantyExpiresAt; + public SetupLocation setupLocation; + public City city; + public ZoneId timeZone; + public double lawnSize; + public double lawnPerimeter; + public AutoSchedule autoScheduleSettings; + public boolean autoSchedule; + public boolean improvement; + public boolean diagnostic; + public long distanceCovered; + public long mowerWorkTime; + + public long bladeWorkTime; + public long bladeWorkTimeReset; + public ZonedDateTime bladeWorkTimeResetAt; + + public int batteryChargeCycles; + public int batteryChargeCyclesReset; + public ZonedDateTime batteryChargeCyclesResetAt; + + public ZonedDateTime createdAt; + public ZonedDateTime updatedAt; + public LastStatus lastStatus; +} diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/api/dto/UsersMeResponse.java b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/api/dto/UsersMeResponse.java new file mode 100644 index 0000000000000..5dac523b9addd --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/api/dto/UsersMeResponse.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.worxlandroid.internal.api.dto; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link UsersMeResponse} class + * + * @author Nils Billing - Initial contribution + * + */ +@NonNullByDefault +public class UsersMeResponse { + public String id = ""; + public String userType = ""; + public boolean pushNotifications; + public String location = ""; + public String actionsOnGooglePinCode = ""; + public String createdAt = ""; + public String updatedAt = ""; +} diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/codes/WorxLandroidActionCodes.java b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/codes/WorxLandroidActionCodes.java new file mode 100644 index 0000000000000..a62b4b4f21e3d --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/codes/WorxLandroidActionCodes.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.worxlandroid.internal.codes; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link WorxLandroidActionCodes} hosts action codes + * + * @author Nils Billing - Initial contribution + */ +@NonNullByDefault +public enum WorxLandroidActionCodes { + START(1, "start"), + STOP(2, "stop"), + HOME(3, "home"), + ZONETRAINING(4, "zonetraining"), + LOCK(5, "lock"), + UNLOCK(6, "unlock"); + + public final int code; + public final String description; + + WorxLandroidActionCodes(int code, String description) { + this.code = code; + this.description = description; + } +} diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/codes/WorxLandroidDayCodes.java b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/codes/WorxLandroidDayCodes.java new file mode 100644 index 0000000000000..e344cc0fd67b7 --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/codes/WorxLandroidDayCodes.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.worxlandroid.internal.codes; + +import java.time.DayOfWeek; +import java.time.format.TextStyle; +import java.util.Locale; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link WorxLandroidDayCodes} hosts Landroid days of week + * + * @author Nils Billing - Initial contribution + */ +@NonNullByDefault +public enum WorxLandroidDayCodes { + SUNDAY(0, DayOfWeek.SUNDAY), + MONDAY(1, DayOfWeek.MONDAY), + TUESDAY(2, DayOfWeek.TUESDAY), + WEDNESDAY(3, DayOfWeek.WEDNESDAY), + THURSDAY(4, DayOfWeek.THURSDAY), + FRIDAY(5, DayOfWeek.FRIDAY), + SATURDAY(6, DayOfWeek.SATURDAY); + + public final int code; + public final DayOfWeek dayOfWeek; + + WorxLandroidDayCodes(int code, DayOfWeek dayOfWeek) { + this.code = code; + this.dayOfWeek = dayOfWeek; + } + + public String getDescription() { + return dayOfWeek.getDisplayName(TextStyle.FULL, Locale.US); + } +} diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/codes/WorxLandroidErrorCodes.java b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/codes/WorxLandroidErrorCodes.java new file mode 100644 index 0000000000000..243c1b64b3c41 --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/codes/WorxLandroidErrorCodes.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.worxlandroid.internal.codes; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link WorxLandroidErrorCodes} hosts error codes + * + * @author Nils Billing - Initial contribution + */ +@NonNullByDefault +public enum WorxLandroidErrorCodes { + @SerializedName("-1") + UNKNOWN, + @SerializedName("0") + NO_ERR, + @SerializedName("1") + TRAPPED, + @SerializedName("2") + LIFTED, + @SerializedName("3") + WIRE_MISSING, + @SerializedName("4") + OUTSIDE_WIRE, + @SerializedName("5") + RAINING, + @SerializedName("6") + CLOSE_DOOR_TO_MOW, + @SerializedName("7") + CLOSE_DOOR_TO_GO_HOME, + @SerializedName("8") + BLADE_MOTOR_BLOCKED, + @SerializedName("9") + WHEEL_MOTOR_BLOCKED, + @SerializedName("10") + TRAPPED_TIMEOUT, + @SerializedName("11") + UPSIDE_DOWN, + @SerializedName("12") + BATTERY_LOW, + @SerializedName("13") + REVERSE_WIRE, + @SerializedName("14") + CHARGE_ERROR, + @SerializedName("15") + TIMEOUT_FINDING_HOME, + @SerializedName("16") + MOWER_LOCKED, + @SerializedName("17") + BATTERY_OVER_TEMPERATURE, + @SerializedName("20") + MOWER_OUTSIDE_WIRE; +} diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/codes/WorxLandroidStatusCodes.java b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/codes/WorxLandroidStatusCodes.java new file mode 100644 index 0000000000000..6a1f077ef97e8 --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/codes/WorxLandroidStatusCodes.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.worxlandroid.internal.codes; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link WorxLandroidStatusCodes} hosts status codes + * + * @author Nils Billing - Initial contribution + */ +@NonNullByDefault +public enum WorxLandroidStatusCodes { + @SerializedName("-1") + UNKNOWN, + @SerializedName("0") + IDLE, + @SerializedName("1") + HOME, + @SerializedName("2") + START_SEQUENCE, + @SerializedName("3") + LEAVING_HOME, + @SerializedName("4") + FOLLOW_WIRE, + @SerializedName("5") + SEARCHING_HOME, + @SerializedName("6") + SEARCHING_WIRE, + @SerializedName("7") + MOWING, + @SerializedName("8") + LIFTED, + @SerializedName("9") + TRAPPED, + @SerializedName("10") + BLADE_BLOCKED, + @SerializedName("11") + DEBUG, + @SerializedName("12") + REMOTE_CONTROL, + @SerializedName("13") + ESCAPE_FROM_OLM, + @SerializedName("30") + GOING_HOME, + @SerializedName("31") + ZONE_TRAINING, + @SerializedName("32") + BORDER_CUT, + @SerializedName("33") + SEARCHING_ZONE, + @SerializedName("34") + PAUSE, + @SerializedName("99") + MANUAL_STOP; +} diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/config/MowerConfiguration.java b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/config/MowerConfiguration.java new file mode 100644 index 0000000000000..cf1bf0f0e7e43 --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/config/MowerConfiguration.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.worxlandroid.internal.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link MowerConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Nils Billing - Initial contribution + * @author Gaël L'hopital - Added serialNumber configuration element + */ +@NonNullByDefault +public class MowerConfiguration { + public static final String SERIAL_NUMBER = "serialNumber"; + + public String serialNumber = ""; + public int refreshStatusInterval = 600; + public int pollingInterval = 3600; + + @Override + public String toString() { + return "MowerConfiguration [serialNumber='%s', pollingInterval='%d', refreshStatusInterval='%d']" + .formatted(serialNumber, pollingInterval, refreshStatusInterval); + } +} diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/config/WebApiConfiguration.java b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/config/WebApiConfiguration.java new file mode 100644 index 0000000000000..74fb15d11b44d --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/config/WebApiConfiguration.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.worxlandroid.internal.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link WebApiConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Nils Billing - Initial contribution + * @author Gaël L'hopital - Added NonNullByDefault, removed setters, removed reconnectInterval + */ +@NonNullByDefault +public class WebApiConfiguration { + public String username = ""; + public String password = ""; + + @Override + public String toString() { + return "WebApiConfiguration [username='%s', password='*****']".formatted(username); + } +} diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/discovery/MowerDiscoveryService.java b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/discovery/MowerDiscoveryService.java new file mode 100644 index 0000000000000..d396e72edc4b8 --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/discovery/MowerDiscoveryService.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.worxlandroid.internal.discovery; + +import static org.openhab.binding.worxlandroid.internal.WorxLandroidBindingConstants.THING_TYPE_MOWER; + +import java.util.List; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.worxlandroid.internal.WorxLandroidBindingConstants; +import org.openhab.binding.worxlandroid.internal.api.WebApiException; +import org.openhab.binding.worxlandroid.internal.api.dto.ProductItemStatus; +import org.openhab.binding.worxlandroid.internal.config.MowerConfiguration; +import org.openhab.binding.worxlandroid.internal.handler.WorxLandroidBridgeHandler; +import org.openhab.core.config.discovery.AbstractDiscoveryService; +import org.openhab.core.config.discovery.DiscoveryResult; +import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link MowerDiscoveryService} is a service for discovering your mowers through Worx Landroid API + * + * @author Nils Billing - Initial contribution + * @author Gaël L'hopital - Added representation property and serialNumber configuration element + */ +@NonNullByDefault +public class MowerDiscoveryService extends AbstractDiscoveryService { + /** + * Maximum time to search for devices in seconds. + */ + private static final int SEARCH_TIME_SEC = 20; + + private final Logger logger = LoggerFactory.getLogger(MowerDiscoveryService.class); + private final WorxLandroidBridgeHandler bridgeHandler; + + public MowerDiscoveryService(WorxLandroidBridgeHandler bridgeHandler) { + super(WorxLandroidBindingConstants.SUPPORTED_THING_TYPES, SEARCH_TIME_SEC); + this.bridgeHandler = bridgeHandler; + } + + @Override + public Set getSupportedThingTypes() { + return WorxLandroidBindingConstants.SUPPORTED_THING_TYPES; + } + + @Override + protected void startScan() { + try { + List productItemsStatusResponse = bridgeHandler.retrieveAllDevices(); + productItemsStatusResponse.forEach(mower -> { + + DiscoveryResult discoveryResult = DiscoveryResultBuilder + .create(new ThingUID(THING_TYPE_MOWER, bridgeHandler.getThing().getUID(), mower.id)) + .withRepresentationProperty(MowerConfiguration.SERIAL_NUMBER).withLabel(mower.name) + .withProperty(MowerConfiguration.SERIAL_NUMBER, mower.serialNumber) + .withBridge(bridgeHandler.getThing().getUID()).build(); + + thingDiscovered(discoveryResult); + logger.debug("Discovered a mower thing with ID '{}'", mower.serialNumber); + }); + } catch (WebApiException exception) { + logger.warn("Error in WebApiException : {}", exception.getMessage()); + } + } + + @Override + protected void startBackgroundDiscovery() { + startScan(); + } +} diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/handler/AWSClientThingHandler.java b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/handler/AWSClientThingHandler.java new file mode 100644 index 0000000000000..09baef7a84c7b --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/handler/AWSClientThingHandler.java @@ -0,0 +1,231 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.worxlandroid.internal.handler; + +import static org.openhab.binding.worxlandroid.internal.WorxLandroidBindingConstants.*; + +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.Instant; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.worxlandroid.internal.api.WebApiException; +import org.openhab.binding.worxlandroid.internal.api.WorxApiDeserializer; +import org.openhab.binding.worxlandroid.internal.api.dto.Payload; +import org.openhab.binding.worxlandroid.internal.mqtt.AWSClient; +import org.openhab.binding.worxlandroid.internal.mqtt.AWSClientCallbackI; +import org.openhab.core.auth.client.oauth2.AccessTokenRefreshListener; +import org.openhab.core.auth.client.oauth2.AccessTokenResponse; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.ThingStatusInfo; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import software.amazon.awssdk.crt.mqtt.MqttMessage; + +/** + * The{@link AWSClientThingHandler} is handles outside communications (AWS,API) parts + * + * @author Gaël L'hopital - Initial contribution + * + */ +@NonNullByDefault +public abstract class AWSClientThingHandler extends BaseThingHandler + implements AWSClientCallbackI, ThingHandlerHelper, AccessTokenRefreshListener { + private static final Duration MIN_PUBLISH_DELAY_S = Duration.ofSeconds(2); + + private final Logger logger = LoggerFactory.getLogger(AWSClientThingHandler.class); + private final AWSClient awsClient; + protected final WorxApiDeserializer deserializer; + + protected String endpoint = ""; + protected String uuid = ""; + protected String userId = ""; + protected String topic = ""; + protected String token = ""; + + private Instant lastPublishTS = Instant.MIN; + private int lastReqHash = 0; + + public AWSClientThingHandler(Thing thing, WorxApiDeserializer deserializer) { + super(thing); + this.deserializer = deserializer; + this.awsClient = new AWSClient(this); + } + + @Override + public void initialize() { + checkInitBridgeOAuth(); + } + + @Override + public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) { + checkInitBridgeOAuth(); + } + + private void checkInitBridgeOAuth() { + WorxLandroidBridgeHandler bridgeHandler = getBridgeHandler(getBridge(), WorxLandroidBridgeHandler.class); + if (bridgeHandler != null) { + setAccessToken(bridgeHandler.getAccessToken()); + bridgeHandler.oAuthClientService.addAccessTokenRefreshListener(this); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE); + } + } + + @Override + public void dispose() { + if (!topic.isEmpty()) { + awsClient.unsubscribe(topic); + } + awsClient.dispose(); + + WorxLandroidBridgeHandler bridgeHandler = getBridgeHandler(getBridge(), WorxLandroidBridgeHandler.class); + if (bridgeHandler != null) { + bridgeHandler.oAuthClientService.removeAccessTokenRefreshListener(this); + } + + super.dispose(); + } + + @Override + public void onAWSConnectionSuccess() { + logger.debug("AWS connection is available"); + if (!topic.isEmpty()) { + awsClient.subscribe(topic, this::onMqttMessage); + logger.debug("subscribed to topic: {}", topic); + } else { + logger.warn("Connected but no topic to subscribe to"); + } + + if (getThing().getStatus() != ThingStatus.ONLINE) { + updateStatus(ThingStatus.ONLINE); + } + updateChannelOnOff(GROUP_AWS, CHANNEL_CONNECTED, awsClient.isConnected()); + } + + @Override + public void onAWSConnectionClosed() { + // Don't try to reconnect if the connection is closed by the thing being disable + if (thing.getStatus() == ThingStatus.ONLINE) { + updateChannelOnOff(GROUP_AWS, CHANNEL_CONNECTED, awsClient.isConnected()); + + WorxLandroidBridgeHandler bridgeHandler = getBridgeHandler(getBridge(), WorxLandroidBridgeHandler.class); + if (bridgeHandler != null) { + bridgeHandler.requestTokenRefresh(); + } + } + } + + @Override + public void onAWSConnectionFailed(@Nullable String message) { + updateChannelOnOff(GROUP_AWS, CHANNEL_CONNECTED, awsClient.isConnected()); + updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, "No AWS Connection"); + } + + @Override + public boolean isLinked(ChannelUID channelUID) { + return super.isLinked(channelUID); + } + + @Override + public void updateState(ChannelUID channelUID, State state) { + super.updateState(channelUID, state); + } + + public void publishMessage(String topic, String cmd) { + Instant now = Instant.now(); + int requestHash = topic.hashCode() + cmd.hashCode(); + if (requestHash == lastReqHash) { + if (now.isBefore(lastPublishTS.plus(MIN_PUBLISH_DELAY_S))) { + logger.debug("Won't post again too soon"); + return; + } + } + lastPublishTS = now; + lastReqHash = requestHash; + logger.debug("publish on topic: '{}' - message: '{}'", topic, cmd); + awsClient.publish(topic, cmd); + } + + public void onMqttMessage(MqttMessage mqttMessage) { + String messagePayload = new String(mqttMessage.getPayload(), StandardCharsets.UTF_8); + logger.debug("onMessage: {}", messagePayload); + try { + Payload payload = deserializer.deserialize(Payload.class, messagePayload); + internalHandlePayload(payload); + } catch (WebApiException e) { + logger.warn("Error processing incoming AWS message: {}", e.getMessage()); + } + } + + protected abstract void internalHandlePayload(Payload payload); + + @Override + public void onAccessTokenResponse(AccessTokenResponse tokenResponse) { + setAccessToken(tokenResponse.getAccessToken()); + } + + private void setAccessToken(String token) { + this.token = token; + connectAWS(); + } + + public void connectAws(String mqttEndpoint, String uuid, String userId, String commandOut) { + this.endpoint = mqttEndpoint; + this.uuid = uuid; + this.userId = userId; + this.topic = commandOut; + connectAWS(); + } + + private void connectAWS() { + awsClient.disconnect(); + if (endpoint.isEmpty() || userId.isEmpty() || uuid.isEmpty() || token.isEmpty()) { + logger.debug("Some data missing to initiate AWS connection"); + return; + } + awsClient.connect(endpoint, userId, uuid, token); + updateStatus(ThingStatus.ONLINE); + updateChannelOnOff(GROUP_AWS, CHANNEL_CONNECTED, awsClient.isConnected()); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (command instanceof RefreshType) { + return; + } + if (!isOnline()) { + logger.warn("handleCommand mower: {} is offline!", getThing().getUID()); + return; + } + WorxLandroidBridgeHandler bridgeHandler = getBridgeHandler(getBridge(), WorxLandroidBridgeHandler.class); + if (bridgeHandler == null) { + logger.error("no bridgeHandler"); + return; + } + String groupId = channelUID.getGroupId(); + String channelId = channelUID.getIdWithoutGroup(); + internalHandleCommand(groupId, channelId, command); + } + + protected abstract void internalHandleCommand(@Nullable String groupId, String channelId, Command command); +} diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/handler/ThingHandlerHelper.java b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/handler/ThingHandlerHelper.java new file mode 100644 index 0000000000000..5896163eecda0 --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/handler/ThingHandlerHelper.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.worxlandroid.internal.handler; + +import java.time.ZonedDateTime; +import java.util.HashSet; +import java.util.Set; + +import javax.measure.Unit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelGroupUID; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.binding.BaseBridgeHandler; +import org.openhab.core.thing.binding.BridgeHandler; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; + +/** + * {@link ThingHandlerHelper} provides utility function for thing handlers + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public interface ThingHandlerHelper { + public boolean isLinked(ChannelUID channelUID); + + public Thing getThing(); + + public void updateState(ChannelUID channelUID, State state); + + public default @Nullable T getBridgeHandler(@Nullable Bridge bridge, + Class expected) { + if (bridge != null) { + BridgeHandler handler = bridge.getHandler(); + if (handler != null) { + try { + T expectedBridge = expected.cast(handler); + if (expectedBridge.getThing().getStatus() == ThingStatus.ONLINE) { + return expectedBridge; + } + } catch (ClassCastException exc) { + } + } + } + return null; + } + + public default boolean isOnline() { + return getThing().getStatus() == ThingStatus.ONLINE; + } + + public default ChannelGroupUID getGroupUID(String group) { + return new ChannelGroupUID(getThing().getUID(), group); + } + + public default ChannelUID getChannelUID(String group, String channelId) { + return new ChannelUID(getThing().getUID(), group, channelId); + } + + public default Set getChannelUIDs(String groupName, Set channelIds) { + Set result = new HashSet<>(); + ChannelGroupUID groupUID = getGroupUID(groupName); + channelIds.forEach(id -> result.add(new ChannelUID(groupUID, id))); + return result; + } + + public default void updateIfActive(String group, String channelId, State state) { + ChannelUID id = getChannelUID(group, channelId); + if (isLinked(id)) { + updateState(id, state); + } + } + + public default void updateChannelOnOff(String group, String channelId, boolean value) { + updateIfActive(group, channelId, OnOffType.from(value)); + } + + public default void updateChannelDateTime(String group, String channelId, @Nullable ZonedDateTime timestamp) { + updateIfActive(group, channelId, timestamp == null ? UnDefType.NULL : new DateTimeType(timestamp)); + } + + public default void updateChannelString(String group, String channelId, @Nullable String value) { + updateIfActive(group, channelId, value == null || value.isEmpty() ? UnDefType.NULL : new StringType(value)); + } + + public default void updateChannelEnum(String group, String channelId, @Nullable Enum value) { + String name = value != null ? value.name() : null; + updateChannelString(group, channelId, name == null || "UNKNOWN".equals(name) ? null : name); + } + + public default void updateChannelDecimal(String group, String channelId, @Nullable Number value) { + updateIfActive(group, channelId, value == null || value.equals(-1) ? UnDefType.NULL : new DecimalType(value)); + } + + public default void updateChannelQuantity(String group, String channelId, @Nullable QuantityType quantity) { + updateIfActive(group, channelId, quantity != null ? quantity : UnDefType.NULL); + } + + public default void updateChannelQuantity(String group, String channelId, @Nullable Number d, Unit unit) { + if (d == null) { + updateIfActive(group, channelId, UnDefType.NULL); + } else { + updateChannelQuantity(group, channelId, new QuantityType<>(d, unit)); + } + } +} diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/handler/WorxLandroidBridgeHandler.java b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/handler/WorxLandroidBridgeHandler.java new file mode 100644 index 0000000000000..2356f20b95f0b --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/handler/WorxLandroidBridgeHandler.java @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.worxlandroid.internal.handler; + +import java.io.IOException; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.worxlandroid.internal.api.WebApiException; +import org.openhab.binding.worxlandroid.internal.api.WorxApiHandler; +import org.openhab.binding.worxlandroid.internal.api.dto.ProductItemStatus; +import org.openhab.binding.worxlandroid.internal.api.dto.UsersMeResponse; +import org.openhab.binding.worxlandroid.internal.config.WebApiConfiguration; +import org.openhab.core.auth.client.oauth2.AccessTokenRefreshListener; +import org.openhab.core.auth.client.oauth2.AccessTokenResponse; +import org.openhab.core.auth.client.oauth2.OAuthClientService; +import org.openhab.core.auth.client.oauth2.OAuthException; +import org.openhab.core.auth.client.oauth2.OAuthFactory; +import org.openhab.core.auth.client.oauth2.OAuthResponseException; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseBridgeHandler; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link WorxLandroidBridgeHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Nils Billing - Initial contribution + * @author Gaël L'hopital - Refactored with oAuthFactory, removed AWSClient + */ +@NonNullByDefault +public class WorxLandroidBridgeHandler extends BaseBridgeHandler + implements AccessTokenRefreshListener, ThingHandlerHelper { + private static final String URL_OAUTH_TOKEN = "https://id.worx.com/oauth/token"; + private static final String CLIENT_ID = "013132A8-DB34-4101-B993-3C8348EA0EBC"; + + private final Logger logger = LoggerFactory.getLogger(WorxLandroidBridgeHandler.class); + private final WorxApiHandler apiHandler; + private final OAuthFactory oAuthFactory; + + public final OAuthClientService oAuthClientService; + + private String accessToken = ""; + private int retryCount = 3; + private int retryDelayS = 1; + private Optional> tokenRefreshJob = Optional.empty(); + private Optional> connectionJob = Optional.empty(); + + public WorxLandroidBridgeHandler(Bridge bridge, WorxApiHandler apiHandler, OAuthFactory oAuthFactory) { + super(bridge); + this.apiHandler = apiHandler; + this.oAuthFactory = oAuthFactory; + this.oAuthClientService = oAuthFactory.createOAuthClientService(getThing().getUID().getAsString(), + URL_OAUTH_TOKEN, null, CLIENT_ID, null, "*", true); + oAuthClientService.addAccessTokenRefreshListener(this); + } + + @Override + public void initialize() { + logger.debug("Initializing Landroid API bridge handler."); + WebApiConfiguration config = getConfigAs(WebApiConfiguration.class); + + if (config.username.isBlank()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/conf-error-no-username"); + return; + } + + if (config.password.isBlank()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/conf-error-no-password"); + return; + } + + updateStatus(ThingStatus.UNKNOWN); + scheduler.execute(() -> initiateConnection(config.username, config.password)); + } + + private void initiateConnection(String username, String password) { + stopConnectionJob(); + try { + accessToken = oAuthClientService.getAccessTokenByResourceOwnerPasswordCredentials(username, password, "*") + .getAccessToken(); + + UsersMeResponse user = apiHandler.retrieveMe(accessToken); + updateProperties(apiHandler.getDeserializer().toMap(user)); + + updateStatus(ThingStatus.ONLINE); + } catch (IOException | WebApiException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } catch (OAuthResponseException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/oauth-connection-error"); + } catch (OAuthException e) { + Throwable cause = e.getCause(); + if (cause != null) { + String message = cause.getMessage(); + if (message != null && message.contains("http code 403")) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/oauth-connection-delayed"); + connectionJob = Optional + .of(scheduler.schedule(() -> initiateConnection(username, password), 1, TimeUnit.HOURS)); + return; + } + } + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/oauth-connection-error"); + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + logger.debug("Landroid Bridge is read-only and does not handle commands"); + } + + @Override + public void dispose() { + stopConnectionJob(); + stopTokenRefreshJob(); + + oAuthClientService.removeAccessTokenRefreshListener(this); + oAuthFactory.ungetOAuthService(getThing().getUID().getAsString()); + super.dispose(); + } + + private void stopTokenRefreshJob() { + tokenRefreshJob.ifPresent(job -> job.cancel(true)); + tokenRefreshJob = Optional.empty(); + } + + private void stopConnectionJob() { + connectionJob.ifPresent(job -> job.cancel(true)); + connectionJob = Optional.empty(); + } + + @Override + public void onAccessTokenResponse(AccessTokenResponse tokenResponse) { + accessToken = tokenResponse.getAccessToken(); + } + + public void requestTokenRefresh() { + if (tokenRefreshJob.isPresent()) { + return; + } + + try { + oAuthClientService.refreshToken(); + retryCount = 3; + stopTokenRefreshJob(); + } catch (IOException | OAuthResponseException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } catch (OAuthException e) { + if (retryCount > 0) { + tokenRefreshJob = Optional.of(scheduler.schedule(() -> { + retryCount--; + requestTokenRefresh(); + }, retryDelayS, TimeUnit.MINUTES)); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/oauth-refresh-error"); + } + } + } + + public String getAccessToken() { + return accessToken; + } + + public @Nullable ProductItemStatus retrieveDeviceStatus(String serialNumber) throws WebApiException { + return apiHandler.retrieveDeviceStatus(accessToken, serialNumber); + } + + public List retrieveAllDevices() throws WebApiException { + return apiHandler.retrieveDeviceStatus(accessToken); + } + + public boolean resetBladeTime(String serialNumber) { + return apiHandler.resetBladeTime(accessToken, serialNumber); + } + + public boolean resetBatteryCycles(String serialNumber) { + return apiHandler.resetBatteryCycles(accessToken, serialNumber); + } + + @Override + public boolean isLinked(ChannelUID channelUID) { + return super.isLinked(channelUID); + } + + @Override + public void updateState(ChannelUID channelUID, State state) { + super.updateState(channelUID, state); + } +} diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/handler/WorxLandroidMowerHandler.java b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/handler/WorxLandroidMowerHandler.java new file mode 100644 index 0000000000000..4eb4b6342d659 --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/handler/WorxLandroidMowerHandler.java @@ -0,0 +1,574 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.worxlandroid.internal.handler; + +import static org.openhab.binding.worxlandroid.internal.WorxLandroidBindingConstants.*; + +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.stream.IntStream; + +import javax.measure.Unit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.worxlandroid.internal.api.WebApiException; +import org.openhab.binding.worxlandroid.internal.api.WorxApiDeserializer; +import org.openhab.binding.worxlandroid.internal.api.dto.Commands.MowerCommand; +import org.openhab.binding.worxlandroid.internal.api.dto.Commands.OneTimeCommand; +import org.openhab.binding.worxlandroid.internal.api.dto.Commands.ScheduleCommand; +import org.openhab.binding.worxlandroid.internal.api.dto.Commands.ScheduleDaysCommand; +import org.openhab.binding.worxlandroid.internal.api.dto.Commands.SetRainDelay; +import org.openhab.binding.worxlandroid.internal.api.dto.Commands.ZoneMeterAlloc; +import org.openhab.binding.worxlandroid.internal.api.dto.Commands.ZoneMeterCommand; +import org.openhab.binding.worxlandroid.internal.api.dto.Payload; +import org.openhab.binding.worxlandroid.internal.api.dto.Payload.Dat.Axis; +import org.openhab.binding.worxlandroid.internal.api.dto.ProductItemStatus; +import org.openhab.binding.worxlandroid.internal.codes.WorxLandroidActionCodes; +import org.openhab.binding.worxlandroid.internal.codes.WorxLandroidDayCodes; +import org.openhab.binding.worxlandroid.internal.codes.WorxLandroidStatusCodes; +import org.openhab.binding.worxlandroid.internal.config.MowerConfiguration; +import org.openhab.binding.worxlandroid.internal.vo.Mower; +import org.openhab.binding.worxlandroid.internal.vo.ScheduledDay; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.ThingStatusInfo; +import org.openhab.core.thing.binding.builder.ThingBuilder; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The{@link WorxLandroidMowerHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Nils Billing - Initial contribution + * + */ +@NonNullByDefault +public class WorxLandroidMowerHandler extends AWSClientThingHandler { + private static final String EMPTY_PAYLOAD = "{}"; + + private final Logger logger = LoggerFactory.getLogger(WorxLandroidMowerHandler.class); + private Optional> refreshJob = Optional.empty(); + private Optional> pollingJob = Optional.empty(); + + private Optional mower = Optional.empty(); + + public WorxLandroidMowerHandler(Thing thing, WorxApiDeserializer deserializer) { + super(thing, deserializer); + } + + @Override + public void initialize() { + super.initialize(); + MowerConfiguration config = getConfigAs(MowerConfiguration.class); + + if (config.serialNumber.isBlank()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/conf-error-no-serial"); + return; + } + + WorxLandroidBridgeHandler bridgeHandler = getBridgeHandler(getBridge(), WorxLandroidBridgeHandler.class); + if (bridgeHandler != null) { + initializeData(bridgeHandler); + } + } + + @Override + public void dispose() { + refreshJob.ifPresent(job -> job.cancel(true)); + refreshJob = Optional.empty(); + + pollingJob.ifPresent(job -> job.cancel(true)); + pollingJob = Optional.empty(); + + super.dispose(); + } + + private void initializeData(WorxLandroidBridgeHandler bridgeHandler) { + MowerConfiguration config = getConfigAs(MowerConfiguration.class); + logger.debug("Initializing WorxLandroidMowerHandler for serial number '{}'", config.serialNumber); + try { + ProductItemStatus product = bridgeHandler.retrieveDeviceStatus(config.serialNumber); + if (product != null) { + connectAws(product.mqttEndpoint, product.uuid, product.userId, product.mqttTopics.commandOut); + mower = Optional.of(new Mower(this, product)); + setChannelsAndProperties(mower.get()); + processStatusMessage(mower.get()); + + updateStatus(product.online ? ThingStatus.ONLINE : ThingStatus.OFFLINE); + startScheduledJobs(bridgeHandler, mower.get(), config); + } + } catch (WebApiException e) { + logger.warn("initialize mower: id {} - {}::{}", config.serialNumber, getThing().getLabel(), + getThing().getUID()); + } + } + + private void setChannelsAndProperties(Mower mower) { + ThingBuilder thingBuilder = editThing(); + Set toRemove = new HashSet<>(); + + if (!mower.lockSupported()) { // lock channel only when supported + toRemove.add(getChannelUID(GROUP_COMMON, CHANNEL_LOCK)); + } + + if (!mower.rainDelaySupported()) { // rainDelay channel only when supported + toRemove.add(getChannelUID(GROUP_RAIN, CHANNEL_DELAY)); + } + + if (!mower.rainDelayStartSupported()) { // // rainDelayStart channel only when supported + toRemove.addAll(getChannelUIDs(GROUP_RAIN, Set.of(CHANNEL_RAIN_STATE, CHANNEL_RAIN_COUNTER))); + } + + if (!mower.multiZoneSupported()) { // multizone channels only when supported + toRemove.add(getChannelUID(GROUP_MULTI_ZONES, CHANNEL_LAST_ZONE)); + + // remove zone meter channels + IntStream.range(0, mower.getMultiZoneCount()) + .forEach(index -> toRemove.add(getChannelUID(GROUP_MULTI_ZONES, "zone-%d".formatted(index + 1)))); + // remove allocation channels + IntStream.range(0, 10).forEach(index -> toRemove + .add(getChannelUID(GROUP_MULTI_ZONES, "%s-%d".formatted(CHANNEL_PREFIX_ALLOCATION, index)))); + } + + if (!mower.oneTimeSchedulerSupported()) { // oneTimeScheduler channel only when supported + toRemove.addAll(getChannelUIDs(GROUP_ONE_TIME, Set.of(CHANNEL_DURATION, CHANNEL_EDGECUT, CHANNEL_MODE))); + } + + if (!mower.scheduler2Supported()) { // Scheduler 2 channels only when supported version + EnumSet.allOf(WorxLandroidDayCodes.class).stream() + .map(dayCode -> "%s2".formatted(dayCode.getDescription().toLowerCase())) + .forEach(groupName -> toRemove.addAll(getChannelUIDs(groupName, + Set.of(CHANNEL_ENABLE, CHANNEL_DURATION, CHANNEL_EDGECUT, CHANNEL_TIME)))); + } + + toRemove.stream().forEach(thingBuilder::withoutChannel); + updateThing(thingBuilder.build()); + + updateProperties(Map.of(Thing.PROPERTY_MAC_ADDRESS, mower.getMacAddress(), Thing.PROPERTY_VENDOR, "Worx", + "productId", mower.getId(), "language", mower.getLanguage(), "mqtt_endpoint", endpoint)); + } + + private void processStatusMessage(Mower mower) { + updateStateCfg(mower); + updateStateDat(mower); + thing.setProperty(Thing.PROPERTY_FIRMWARE_VERSION, mower.getFirmwareVersion()); + } + + /** + * Start scheduled jobs. + * Jobs are only started if interval > 0 + */ + private void startScheduledJobs(WorxLandroidBridgeHandler bridgeHandler, Mower theMower, + MowerConfiguration config) { + if (config.refreshStatusInterval > 0) { + refreshJob = Optional.of(scheduler.scheduleWithFixedDelay(() -> { + try { + ProductItemStatus product = bridgeHandler.retrieveDeviceStatus(config.serialNumber); + updateChannelDateTime(GROUP_COMMON, CHANNEL_ONLINE_TIMESTAMP, ZonedDateTime.now()); + updateChannelOnOff(GROUP_COMMON, CHANNEL_ONLINE, product != null && product.online); + updateStatus(product != null ? ThingStatus.ONLINE : ThingStatus.OFFLINE); + } catch (WebApiException e) { + logger.debug("Refreshing Thing {} failed, handler might be OFFLINE", config.serialNumber); + } + }, 3, config.refreshStatusInterval, TimeUnit.SECONDS)); + } + + if (config.pollingInterval > 0) { + pollingJob = Optional.of(scheduler.scheduleWithFixedDelay(() -> sendCommand(theMower, EMPTY_PAYLOAD), 5, + config.pollingInterval, TimeUnit.SECONDS)); + } + } + + @Override + public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) { + super.bridgeStatusChanged(bridgeStatusInfo); + WorxLandroidBridgeHandler bridgeHandler = getBridgeHandler(getBridge(), WorxLandroidBridgeHandler.class); + if (bridgeHandler != null) { + initializeData(bridgeHandler); + } + } + + @Override + protected void internalHandleCommand(@Nullable String groupId, String channelId, Command command) { + mower.ifPresent(theMower -> { + if (GROUP_MULTI_ZONES.equals(groupId)) { + handleMultiZonesCommand(theMower, channelId, command); + } else if (GROUP_AWS.equals(groupId)) { + handleAWSCommand(theMower, channelId); + } else if (GROUP_SCHEDULE.equals(groupId)) { + handleScheduleCommand(theMower, channelId, Integer.parseInt(command.toString())); + } else if (GROUP_ONE_TIME.equals(groupId)) { + handleOneTimeSchedule(theMower, channelId, command); + } else if (GROUP_COMMON.equals(groupId)) { + handleCommonGroup(theMower, channelId, command); + } else if (groupId != null && groupId.contains("day")) { + setScheduledDays(theMower, groupId, channelId, command); + sendCommand(theMower, + theMower.scheduler2Supported() + ? new ScheduleDaysCommand(theMower.getTimeExtension(), theMower.getScheduleArray1(), + theMower.getScheduleArray2()) + : new ScheduleDaysCommand(theMower.getTimeExtension(), theMower.getScheduleArray1())); + } else if (CHANNEL_DELAY.equals(channelId)) { + int delaySec = commandToInt(command, Units.SECOND); + sendCommand(theMower, new SetRainDelay(delaySec)); + } else if (CHANNEL_BLADE_TIME.equals(channelId) || CHANNEL_CHARGE_CYCLES.equals(channelId)) { + resetStat(channelId, theMower.getSerialNumber()); + } else { + logger.debug("command for channel {} not supported: {}", channelId, command); + } + }); + } + + private void handleAWSCommand(Mower theMower, String channel) { + if (CHANNEL_POLL.equals(channel)) { + sendCommand(theMower, EMPTY_PAYLOAD); + updateState(CHANNEL_POLL, OnOffType.OFF); + } else { + logger.warn("No action identified on channel {}", channel); + } + } + + private void handleCommonGroup(Mower theMower, String channel, Command command) { + if (CHANNEL_ACTION.equals(channel)) { + WorxLandroidActionCodes actionCode = WorxLandroidActionCodes.valueOf(command.toString()); + sendCommand(theMower, new MowerCommand(actionCode)); + } else if (CHANNEL_LOCK.equals(channel)) { + WorxLandroidActionCodes lockCode = OnOffType.ON.equals(command) ? WorxLandroidActionCodes.LOCK + : WorxLandroidActionCodes.UNLOCK; + sendCommand(theMower, new MowerCommand(lockCode)); + } else if (CHANNEL_ENABLE.equals(channel)) { + theMower.setEnable(OnOffType.ON.equals(command)); + sendCommand(theMower, + theMower.scheduler2Supported() + ? new ScheduleDaysCommand(theMower.getTimeExtension(), theMower.getScheduleArray1(), + theMower.getScheduleArray2()) + : new ScheduleDaysCommand(theMower.getTimeExtension(), theMower.getScheduleArray1())); + } else { + logger.warn("No action identified for command {} on channel {}", command, channel); + } + } + + private void handleOneTimeSchedule(Mower theMower, String channel, Command command) { + if (CHANNEL_DURATION.equals(channel)) { + sendCommand(theMower, new OneTimeCommand(0, Integer.parseInt(command.toString()))); + } else if (CHANNEL_EDGECUT.equals(channel)) { + sendCommand(theMower, new OneTimeCommand(OnOffType.ON.equals(command) ? 1 : 0, 0)); + } else { + logger.warn("No action identified for command {} on channel {}", command, channel); + } + } + + private void handleScheduleCommand(Mower theMower, String channel, int command) { + if (CHANNEL_MODE.equals(channel)) { + sendCommand(theMower, new ScheduleCommand(command)); + } else if (CHANNEL_TIME_EXTENSION.equals(channel)) { + theMower.setTimeExtension(command); + sendCommand(theMower, + theMower.scheduler2Supported() + ? new ScheduleDaysCommand(theMower.getTimeExtension(), theMower.getScheduleArray1(), + theMower.getScheduleArray2()) + : new ScheduleDaysCommand(theMower.getTimeExtension(), theMower.getScheduleArray1())); + } else { + logger.warn("No action identified for command {} on channel {}", command, channel); + } + } + + private void handleMultiZonesCommand(Mower theMower, String channel, Command command) { + if (CHANNEL_ENABLE.equals(channel)) { + theMower.setMultiZoneEnable(OnOffType.ON.equals(command)); + sendCommand(theMower, new ZoneMeterCommand(theMower.getZoneMeters())); + } else if (CHANNEL_LAST_ZONE.equals(channel)) { + if (!WorxLandroidStatusCodes.HOME.equals(theMower.getStatusCode())) { + logger.warn("Cannot start zone because mower must be at HOME!"); + return; + } + + theMower.setZoneTo(Integer.parseInt(command.toString())); + sendCommand(theMower, new ZoneMeterCommand(theMower.getZoneMeters())); + scheduler.schedule(() -> sendCommand(theMower, new MowerCommand(WorxLandroidActionCodes.START)), 2000, + TimeUnit.MILLISECONDS); + } else { + String[] names = channel.split("-"); + int index = Integer.valueOf(names[1]); + + if (CHANNEL_PREFIX_ZONE.startsWith(names[0])) { + int meterValue = commandToInt(command, SIUnits.METRE); + theMower.setZoneMeter(index - 1, meterValue); + sendCommand(theMower, new ZoneMeterCommand(theMower.getZoneMeters())); + } else if (CHANNEL_PREFIX_ALLOCATION.startsWith(names[0])) { + theMower.setAllocation(index, Integer.parseInt(command.toString())); + sendCommand(theMower, new ZoneMeterAlloc(theMower.getAllocations())); + } else { + logger.warn("No action identified for command {} on channel {}", command, channel); + } + } + } + + private int commandToInt(Command command, @Nullable Unit targetUnit) { + if (command instanceof QuantityType qtty && targetUnit != null) { + QuantityType inTarget = qtty.toUnit(targetUnit); + if (inTarget != null) { + return inTarget.intValue(); + } + } + return Integer.parseInt(command.toString()); + } + + /** + * Set scheduled days + * + * @param theMower + * + * @param scDaysIndex 1 or 2 + * @param channelUID + * @param command + */ + private void setScheduledDays(Mower theMower, String groupId, String channelId, Command command) { + int scDaysSlot = groupId.endsWith("2") ? 2 : 1; + WorxLandroidDayCodes dayCodeUpdated = WorxLandroidDayCodes.valueOf(groupId.replace("2", "").toUpperCase()); + + ScheduledDay scheduledDayUpdated = theMower.getScheduledDay(scDaysSlot, dayCodeUpdated); + if (scheduledDayUpdated == null) { + return; + } + + if (CHANNEL_ENABLE.equals(channelId)) { + scheduledDayUpdated.setEnable(OnOffType.ON.equals(command)); + } else if (CHANNEL_TIME.equals(channelId)) { + if (command instanceof DateTimeType dateTime) { + ZonedDateTime zdt = dateTime.getZonedDateTime(); + scheduledDayUpdated.setStartTime(zdt); + } else if (command instanceof StringType stringType) { + scheduledDayUpdated.setStartTime(stringType.toString()); + } else { + logger.warn("Incorrect command {} on channel {}:{} ", command, groupId, channelId); + } + } else if (CHANNEL_DURATION.equals(channelId)) { + scheduledDayUpdated.setDuration(Integer.parseInt(command.toString())); + } else if (CHANNEL_EDGECUT.equals(channelId)) { + scheduledDayUpdated.setEdgecut(OnOffType.ON.equals(command)); + } + } + + private void sendCommand(Mower theMower, Object command) { + logger.debug("send command: {}", command); + publishMessage(theMower.getMqttCommandIn(), command); + } + + /** + * Update states for data values + * + * @param theMower + * + * @param dat + */ + private void updateStateDat(Mower theMower) { + updateChannelQuantity(GROUP_ORIENTATION, CHANNEL_PITCH, theMower.getAngle(Axis.PITCH), Units.DEGREE_ANGLE); + updateChannelQuantity(GROUP_ORIENTATION, CHANNEL_ROLL, theMower.getAngle(Axis.ROLL), Units.DEGREE_ANGLE); + updateChannelQuantity(GROUP_ORIENTATION, CHANNEL_YAW, theMower.getAngle(Axis.YAW), Units.DEGREE_ANGLE); + updateChannelEnum(GROUP_COMMON, CHANNEL_STATUS_CODE, theMower.getPayloadDat().statusCode); + updateChannelEnum(GROUP_COMMON, CHANNEL_ERROR_CODE, theMower.getPayloadDat().errorCode); + updateChannelDecimal(GROUP_MULTI_ZONES, CHANNEL_LAST_ZONE, theMower.getLastZone()); + + updateChannelDecimal(GROUP_BATTERY, CHANNEL_CHARGE_CYCLES, theMower.getCurrentChargeCycles()); + updateChannelDecimal(GROUP_BATTERY, CHANNEL_CHARGE_CYCLES_TOTAL, theMower.getTotalChargeCycles()); + updateChannelQuantity(GROUP_METRICS, CHANNEL_BLADE_TIME, theMower.getCurrentBladeTime(), Units.MINUTE); + updateChannelQuantity(GROUP_METRICS, CHANNEL_BLADE_TIME_TOTAL, theMower.getTotalBladeTime(), Units.MINUTE); + + theMower.getBattery().ifPresent(battery -> { + updateChannelQuantity(GROUP_BATTERY, CHANNEL_TEMPERATURE, battery.temp != -1 ? battery.temp : null, + SIUnits.CELSIUS); + updateChannelQuantity(GROUP_BATTERY, CHANNEL_VOLTAGE, battery.voltage != -1 ? battery.voltage : null, + Units.VOLT); + updateChannelDecimal(GROUP_BATTERY, CHANNEL_LEVEL, battery.level); + updateChannelOnOff(GROUP_BATTERY, CHANNEL_CHARGING, battery.charging); + }); + + theMower.getStats().ifPresent(stats -> { + updateChannelQuantity(GROUP_METRICS, CHANNEL_DISTANCE, + stats.distanceCovered != -1 ? stats.distanceCovered : null, SIUnits.METRE); + updateChannelQuantity(GROUP_METRICS, CHANNEL_TOTAL_TIME, + stats.mowerWorkTime != -1 ? stats.mowerWorkTime : null, Units.MINUTE); + }); + + int rssi = theMower.getPayloadDat().wifiQuality; + updateChannelDecimal(GROUP_WIFI, CHANNEL_WIFI_QUALITY, rssi <= 0 ? toQoS(rssi) : null); + updateChannelQuantity(GROUP_WIFI, CHANNEL_RSSI, + rssi <= 0 ? new QuantityType<>(rssi, Units.DECIBEL_MILLIWATTS) : null); + + if (theMower.lockSupported()) { + updateChannelOnOff(GROUP_COMMON, CHANNEL_LOCK, theMower.getPayloadDat().isLocked()); + } + + theMower.getRain().ifPresent(rain -> { + if (theMower.rainDelayStartSupported()) { + updateChannelOnOff(GROUP_RAIN, CHANNEL_RAIN_STATE, rain.raining); + updateChannelQuantity(GROUP_RAIN, CHANNEL_RAIN_COUNTER, rain.counter, Units.MINUTE); + } + }); + } + + /** + * Update states for cfg values + * + * @param theMower + * + * @param cfg + * @param zoneId + */ + private void updateStateCfg(Mower theMower) { + updateChannelDateTime(GROUP_CONFIG, CHANNEL_TIMESTAMP, theMower.getLastUpdate()); + + theMower.getOneTimeSchedule().ifPresent(ots -> { + updateChannelOnOff(GROUP_ONE_TIME, CHANNEL_EDGECUT, ots.getEdgeCut()); + updateChannelQuantity(GROUP_ONE_TIME, CHANNEL_DURATION, ots.duration != -1 ? ots.duration : null, + Units.MINUTE); + }); + + theMower.getSchedule().ifPresent(schedule -> { + if (theMower.oneTimeSchedulerSupported()) { + updateChannelEnum(GROUP_SCHEDULE, CHANNEL_MODE, schedule.scheduleMode); + } + + if (schedule.timeExtension != -1) { + updateChannelQuantity(GROUP_SCHEDULE, CHANNEL_TIME_EXTENSION, schedule.timeExtension, Units.PERCENT); + updateChannelOnOff(GROUP_COMMON, CHANNEL_ENABLE, theMower.isEnable()); + } + + if (schedule.d != null) { + updateStateCfgScDays(theMower, 1, schedule.d); + if (schedule.dd != null) { + updateStateCfgScDays(theMower, 2, schedule.dd); + } + } + }); + + int command = theMower.getPayloadCfg().cmd; + updateChannelDecimal(GROUP_CONFIG, CHANNEL_COMMAND, command != -1 ? command : null); + + if (theMower.multiZoneSupported()) { + for (int zoneIndex = 0; zoneIndex < theMower.getZonesSize(); zoneIndex++) { + updateChannelQuantity(GROUP_MULTI_ZONES, CHANNEL_PREFIX_ZONE.formatted(zoneIndex + 1), + theMower.getZoneMeter(zoneIndex), SIUnits.METRE); + } + + for (int allocationIndex = 0; allocationIndex < theMower.getAllocationsSize(); allocationIndex++) { + updateChannelDecimal(GROUP_MULTI_ZONES, CHANNEL_PREFIX_ALLOCATION.formatted(allocationIndex), + theMower.getAllocation(allocationIndex)); + } + updateChannelOnOff(GROUP_MULTI_ZONES, CHANNEL_ENABLE, theMower.isMultiZoneEnable()); + } + + int rainDelay = theMower.getPayloadCfg().rainDelay; + updateChannelQuantity(GROUP_RAIN, CHANNEL_DELAY, + theMower.rainDelaySupported() && rainDelay != -1 ? rainDelay : null, Units.MINUTE); + } + + /** + * @param theMower + * @param scDSlot scheduled day slot + * @param d scheduled day + */ + private void updateStateCfgScDays(Mower theMower, int scDSlot, List> d) { + List nextStarts = new ArrayList<>(); + List nextEnds = new ArrayList<>(); + + for (WorxLandroidDayCodes dayCode : WorxLandroidDayCodes.values()) { + ScheduledDay scheduledDay = theMower.getScheduledDay(scDSlot, dayCode); + if (scheduledDay == null) { + return; + } + + String groupName = "%s%s".formatted(dayCode.getDescription().toLowerCase(), + scDSlot == 1 ? "" : String.valueOf(scDSlot)); + + updateChannelOnOff(groupName, CHANNEL_ENABLE, scheduledDay.isEnabled()); + updateChannelOnOff(groupName, CHANNEL_EDGECUT, scheduledDay.isEdgecut()); + updateChannelQuantity(groupName, CHANNEL_DURATION, scheduledDay.getDuration(), Units.MINUTE); + + if (scheduledDay.isEnabled()) { + ZonedDateTime scheduleStart = ZonedDateTime.now().truncatedTo(ChronoUnit.MINUTES) + .with(scheduledDay.getStartTime()); + scheduleStart = ZonedDateTime.from(dayCode.dayOfWeek.adjustInto(scheduleStart)); + updateChannelDateTime(groupName, CHANNEL_TIME, scheduleStart); + ZonedDateTime scheduleEnd = scheduleStart.plusMinutes(scheduledDay.getDuration()); + if (scheduleStart.isBefore(ZonedDateTime.now())) { + scheduleStart = scheduleStart.plusDays(7); + } + if (scheduleEnd.isBefore(ZonedDateTime.now())) { + scheduleEnd = scheduleEnd.plusDays(7); + } + + nextStarts.add(scheduleStart); + nextEnds.add(scheduleEnd); + } + } + if (!nextStarts.isEmpty()) { + Collections.sort(nextStarts); + Collections.sort(nextEnds); + updateChannelDateTime(GROUP_SCHEDULE, CHANNEL_START, nextStarts.get(0)); + updateChannelDateTime(GROUP_SCHEDULE, CHANNEL_STOP, nextEnds.get(0)); + } + } + + private int toQoS(int rssi) { + return rssi > -50 ? 4 : rssi > -60 ? 3 : rssi > -70 ? 2 : rssi > -85 ? 1 : 0; + } + + public void publishMessage(String topic, Object command) { + publishMessage(topic, deserializer.toJson(command)); + } + + @Override + protected void internalHandlePayload(Payload payload) { + mower.ifPresent(theMower -> { + theMower.setStatus(payload); + updateStateCfg(theMower); + updateStateDat(theMower); + }); + } + + private boolean resetStat(String channelId, String serialNumber) { + WorxLandroidBridgeHandler bridgeHandler = getBridgeHandler(getBridge(), WorxLandroidBridgeHandler.class); + if (bridgeHandler != null) { + logger.debug("Resetting {}", channelId); + if (CHANNEL_BLADE_TIME.equals(channelId)) { + bridgeHandler.resetBladeTime(serialNumber); + } else { + bridgeHandler.resetBatteryCycles(serialNumber); + } + } + return true; + } +} diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/mqtt/AWSClient.java b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/mqtt/AWSClient.java new file mode 100644 index 0000000000000..cb698808cf0b2 --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/mqtt/AWSClient.java @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.worxlandroid.internal.mqtt; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.common.ThreadPoolManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import software.amazon.awssdk.crt.CRT; +import software.amazon.awssdk.crt.http.HttpRequest; +import software.amazon.awssdk.crt.mqtt.MqttClientConnection; +import software.amazon.awssdk.crt.mqtt.MqttClientConnectionEvents; +import software.amazon.awssdk.crt.mqtt.MqttException; +import software.amazon.awssdk.crt.mqtt.MqttMessage; +import software.amazon.awssdk.crt.mqtt.OnConnectionFailureReturn; +import software.amazon.awssdk.crt.mqtt.OnConnectionSuccessReturn; +import software.amazon.awssdk.crt.mqtt.QualityOfService; +import software.amazon.awssdk.iot.AwsIotMqttConnectionBuilder; + +/** + * {@link AWSClient} AWS client + * + * @author Nils Billing - Initial contribution + */ +@NonNullByDefault +public class AWSClient implements MqttClientConnectionEvents { + private static final QualityOfService QOS = QualityOfService.AT_MOST_ONCE; + private static final String AUTHORIZER_NAME = "com-worxlandroid-customer"; + private static final String MQTT_USERNAME = "openhab"; + + private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool("AWSClient"); + private final Map> subscriptions = new HashMap<>(); + private final Logger logger = LoggerFactory.getLogger(AWSClient.class); + private final AWSClientCallbackI clientCallback; + + private @Nullable MqttClientConnection mqttClient; + private LocalDateTime lastResumed = LocalDateTime.MIN; + private boolean connected; + + public AWSClient(AWSClientCallbackI clientCallback) { + this.clientCallback = clientCallback; + } + + public void connect(String endpoint, String userId, String productUuid, String token) { + String[] tok = token.replaceAll("_", "/").replaceAll("-", "+").split("\\."); + + try { + MqttClientConnection connection = AwsIotMqttConnectionBuilder.newDefaultBuilder() + // .withCustomAuthorizer(MQTT_USERNAME, AUTHORIZER_NAME, tok[2], null, MQTT_USERNAME, token) + .withClientId("WX/USER/%s/%s/%s".formatted(userId, MQTT_USERNAME, productUuid)) + .withEndpoint(endpoint).withUsername(MQTT_USERNAME).withCleanSession(false).withKeepAliveSecs(300) + .withConnectionEventCallbacks(this).withWebsockets(true) + .withWebsocketHandshakeTransform(handshakeArgs -> { + HttpRequest httpRequest = handshakeArgs.getHttpRequest(); + httpRequest.addHeader("x-amz-customauthorizer-name", AUTHORIZER_NAME); + httpRequest.addHeader("x-amz-customauthorizer-signature", tok[2]); + httpRequest.addHeader("jwt", tok[0] + "." + tok[1]); + handshakeArgs.complete(httpRequest); + }).build(); + connection.connect().get(); + this.mqttClient = connection; + } catch (MqttException | UnsupportedEncodingException | InterruptedException | ExecutionException e) { + clientCallback.onAWSConnectionFailed(e.getMessage()); + } + } + + public void dispose() { + disconnect(); + subscriptions.clear(); + } + + @Override + public void onConnectionSuccess(@NonNullByDefault({}) OnConnectionSuccessReturn data) { + onConnectionResumed(data.getSessionPresent()); + } + + @Override + public void onConnectionResumed(boolean sessionPresent) { + connected = sessionPresent; + if (sessionPresent) { + lastResumed = LocalDateTime.now(); + logger.debug("last connection resume {}", lastResumed); + subscriptions.forEach(this::subscribe); + clientCallback.onAWSConnectionSuccess(); + } else { + clientCallback.onAWSConnectionClosed(); + } + } + + @Override + public void onConnectionInterrupted(int errorCode) { + LocalDateTime interrupted = LocalDateTime.now(); + connected = false; + String error = CRT.awsErrorString(errorCode); + logger.debug("connection interrupted errorcode: {} : {}", errorCode, error); + + scheduler.schedule(() -> { + /** + * workaround -> after 20 minutes the connection is interrupted but immediately resumed (~0,5sec). + * ConnectionBuilder with ".withKeepAliveSecs(300)" doesn't work + */ + boolean isBetween = lastResumed.isAfter(interrupted) && lastResumed.isBefore(LocalDateTime.now()); + logger.debug("lastResumed: {} interrupted: {} in: {}", lastResumed, interrupted, isBetween); + if (!isBetween) { + clientCallback.onAWSConnectionClosed(); + } + }, 5, TimeUnit.SECONDS); + } + + @Override + public void onConnectionFailure(@NonNullByDefault({}) OnConnectionFailureReturn data) { + connected = false; + if (data.getErrorCode() == 5134) { + clientCallback.onAWSConnectionFailed("Error code 5134: banned 24h"); + } else { + logger.debug("{}", data.toString()); + clientCallback.onAWSConnectionClosed(); + } + }; + + public void disconnect() { + MqttClientConnection connection = mqttClient; + if (connection != null) { + connection.disconnect(); + connection.close(); + mqttClient = null; + } + connected = false; + } + + public void subscribe(String topic, Consumer handler) { + MqttClientConnection connection = mqttClient; + if (connection != null) { + subscriptions.put(topic, handler); + connection.subscribe(topic, QOS, handler); + } else { + logger.warn("Tried to subscribe on {} when connection is closed", topic); + } + } + + public void unsubscribe(String topic) { + MqttClientConnection connection = mqttClient; + if (connection != null) { + subscriptions.remove(topic); + connection.unsubscribe(topic); + } else { + logger.warn("Tried to unsubscribe from {} when connection is closed", topic); + } + } + + public void publish(String topic, String payload) { + MqttClientConnection connection = mqttClient; + if (connection != null) { + connection.publish(new MqttMessage(topic, payload.getBytes(StandardCharsets.UTF_8), QOS)); + } else { + logger.warn("Tried to publish on {} when connection is closed", topic); + } + } + + public boolean isConnected() { + return connected; + } +} diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/mqtt/AWSClientCallbackI.java b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/mqtt/AWSClientCallbackI.java new file mode 100644 index 0000000000000..a5274ee2fa70c --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/mqtt/AWSClientCallbackI.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.worxlandroid.internal.mqtt; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * {@link AWSClientCallbackI} Callback for AWS connection events + * + * @author Nils Billing - Initial contribution + */ +@NonNullByDefault +public interface AWSClientCallbackI { + + /** + * callback method on connection success + */ + public void onAWSConnectionSuccess(); + + /** + * callback method on connection closed + */ + public void onAWSConnectionClosed(); + + /** + * callback method on connection failed + */ + public void onAWSConnectionFailed(@Nullable String message); +} diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/vo/Mower.java b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/vo/Mower.java new file mode 100644 index 0000000000000..45750dc7d9302 --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/vo/Mower.java @@ -0,0 +1,413 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.worxlandroid.internal.vo; + +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.worxlandroid.internal.api.dto.Commands.ZoneMeterCommand; +import org.openhab.binding.worxlandroid.internal.api.dto.LastStatus; +import org.openhab.binding.worxlandroid.internal.api.dto.Payload; +import org.openhab.binding.worxlandroid.internal.api.dto.Payload.Battery; +import org.openhab.binding.worxlandroid.internal.api.dto.Payload.Cfg; +import org.openhab.binding.worxlandroid.internal.api.dto.Payload.Dat; +import org.openhab.binding.worxlandroid.internal.api.dto.Payload.Dat.Axis; +import org.openhab.binding.worxlandroid.internal.api.dto.Payload.Ots; +import org.openhab.binding.worxlandroid.internal.api.dto.Payload.Rain; +import org.openhab.binding.worxlandroid.internal.api.dto.Payload.Schedule; +import org.openhab.binding.worxlandroid.internal.api.dto.Payload.Stat; +import org.openhab.binding.worxlandroid.internal.api.dto.ProductItemStatus; +import org.openhab.binding.worxlandroid.internal.codes.WorxLandroidDayCodes; +import org.openhab.binding.worxlandroid.internal.codes.WorxLandroidStatusCodes; +import org.openhab.binding.worxlandroid.internal.handler.WorxLandroidMowerHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@link Mower} + * + * @author Nils Billing - Initial contribution + */ +@NonNullByDefault +public class Mower { + private static final int[] MULTI_ZONE_METER_DISABLE = { 0, 0, 0, 0 }; + private static final int[] MULTI_ZONE_METER_ENABLE = { 1, 0, 0, 0 }; + private static final int TIME_EXTENSION_DISABLE = -100; + + private final Logger logger = LoggerFactory.getLogger(Mower.class); + private final WorxLandroidMowerHandler mowerHandler; + private final ProductItemStatus product; + + private final int[] zoneMeter; + private final int[] zoneMeterRestore; + private final int[] allocations = new int[10]; + private final List> schedules = new ArrayList<>(); + + private boolean multiZoneEnable; + private int timeExtension; + private int timeExtensionRestore = 0; + private @NonNullByDefault({}) LastStatus lastStatus; + + private boolean restoreZoneMeter = false; + private int[] zoneMeterRestoreValues = {}; + + public Mower(WorxLandroidMowerHandler mowerHandler, ProductItemStatus product) { + this.mowerHandler = mowerHandler; + this.product = product; + this.zoneMeter = new int[getMultiZoneCount()]; + this.zoneMeterRestore = new int[getMultiZoneCount()]; + + schedules.add(new HashMap(7)); + if (product.capabilities.contains("scheduler_two_slots")) { + schedules.add(new HashMap(7)); + } + setStatus(product.lastStatus.payload); + } + + public String getSerialNumber() { + return product.serialNumber; + } + + public int getTimeExtension() { + return timeExtension; + } + + public String getFirmwareVersion() { + return product.firmwareVersion; + } + + /** + * timeExtension = -100 disables mowing (enable=false). + * timeExtension > -100 enables mowing (enable=true). + * + * @param timeExtension + */ + public void setTimeExtension(int timeExtension) { + if (timeExtension == TIME_EXTENSION_DISABLE) { + storeTimeExtension(); + } + this.timeExtension = timeExtension; + } + + public boolean lockSupported() { + return product.capabilities.contains("lock"); + } + + public boolean rainDelaySupported() { + return product.capabilities.contains("rain_delay"); + } + + public boolean rainDelayStartSupported() { + return product.capabilities.contains("rain_delay_start"); + } + + public boolean multiZoneSupported() { + return product.capabilities.contains("multi_zone"); + } + + public boolean scheduler2Supported() { + return schedules.size() > 1; + } + + public boolean oneTimeSchedulerSupported() { + return product.capabilities.contains("one_time_scheduler"); + } + + public @Nullable ScheduledDay getScheduledDay(int scDSlot, WorxLandroidDayCodes dayCode) { + return scDSlot == 1 ? schedules.get(0).get(dayCode) + : scheduler2Supported() ? schedules.get(1).get(dayCode) : null; + } + + private Object[] getScheduleArray(Map schedules) { + Object[] result = new Object[7]; + for (WorxLandroidDayCodes dayCode : WorxLandroidDayCodes.values()) { + ScheduledDay schedule = schedules.get(dayCode); + result[dayCode.code] = schedule != null ? schedule.asArray() : ScheduledDay.BLANK.asArray(); + } + return result; + } + + public Object[] getScheduleArray1() { + return getScheduleArray(schedules.get(0)); + } + + public Object[] getScheduleArray2() { + return scheduler2Supported() ? getScheduleArray(schedules.get(1)) : new Object[] {}; + } + + public boolean isMultiZoneEnable() { + return multiZoneEnable; + } + + public void setMultiZoneEnable(boolean multiZoneEnable) { + this.multiZoneEnable = multiZoneEnable; + + if (multiZoneEnable && isZoneMeterDisabled()) { + restoreZoneMeter(); + if (isZoneMeterDisabled()) { + System.arraycopy(MULTI_ZONE_METER_ENABLE, 0, zoneMeter, 0, zoneMeter.length); + } + } else { + storeZoneMeter(); + System.arraycopy(MULTI_ZONE_METER_DISABLE, 0, zoneMeter, 0, zoneMeter.length); + } + } + + public int getZoneMeter(int zoneIndex) { + return zoneMeter[zoneIndex]; + } + + public int[] getZoneMeters() { + return Arrays.copyOf(zoneMeter, zoneMeter.length); + } + + public int getZonesSize() { + return getZoneMeters().length; + } + + public void setZoneMeters(int[] zoneMeterInput) { + System.arraycopy(zoneMeterInput, 0, zoneMeter, 0, zoneMeter.length); + } + + public void setZoneMeter(int zoneIndex, int meter) { + zoneMeter[zoneIndex] = meter; + this.multiZoneEnable = !isZoneMeterDisabled(); + } + + public int getAllocation(int allocationIndex) { + return allocations[allocationIndex]; + } + + public int[] getAllocations() { + return Arrays.copyOf(allocations, allocations.length); + } + + public int getAllocationsSize() { + return getZoneMeters().length; + } + + public void setAllocation(int allocationIndex, int zoneIndex) { + allocations[allocationIndex] = zoneIndex; + } + + public boolean isEnable() { + return timeExtension != TIME_EXTENSION_DISABLE; + } + + /** + * Enable/Disables mowing using timeExtension. + * disable: timeExtension = -100 + * enable: timeExtension > -100 + * + */ + public void setEnable(boolean enable) { + if (enable && timeExtension == TIME_EXTENSION_DISABLE) { + restoreTimeExtension(); + } else { + storeTimeExtension(); + timeExtension = TIME_EXTENSION_DISABLE; + } + } + + /** + * Stores timeExtension to timeExtensionRestore for restore, + */ + private void storeTimeExtension() { + if (this.timeExtension > TIME_EXTENSION_DISABLE) { + this.timeExtensionRestore = this.timeExtension; + } + } + + /** + * Restores timeExtension from timeExtensionRestore. + */ + private void restoreTimeExtension() { + this.timeExtension = this.timeExtensionRestore; + } + + /** + * Stores zoneMeter to zoneMeterRestore for restore, + */ + private void storeZoneMeter() { + if (!isZoneMeterDisabled()) { + System.arraycopy(zoneMeter, 0, zoneMeterRestore, 0, zoneMeter.length); + } + } + + /** + * Restores zoneMeter from zoneMeterRestore. + */ + private void restoreZoneMeter() { + System.arraycopy(zoneMeterRestore, 0, zoneMeter, 0, zoneMeter.length); + } + + /** + * @return false if less than 2 meters are 0 + */ + private boolean isZoneMeterDisabled() { + return Arrays.stream(zoneMeter).sum() == 0; + } + + public int getMultiZoneCount() { + return multiZoneSupported() ? product.lastStatus.payload.cfg.multizoneAllocations.size() : 0; + } + + public String getMqttCommandIn() { + return product.mqttTopics.commandIn; + } + + public String getMqttCommandOut() { + return product.mqttTopics.commandOut; + } + + public String getMacAddress() { + return product.macAddress; + } + + public String getId() { + return product.id; + } + + public String getLanguage() { + return getPayload().cfg.lg; + } + + public Payload getPayload() { + return lastStatus.payload; + } + + public Dat getPayloadDat() { + return getPayload().dat; + } + + public Cfg getPayloadCfg() { + return getPayload().cfg; + } + + public void setStatus(Payload payload) { + this.lastStatus = new LastStatus(payload); + if (restoreZoneMeter && getStatusCode() != WorxLandroidStatusCodes.HOME + && getStatusCode() != WorxLandroidStatusCodes.START_SEQUENCE + && getStatusCode() != WorxLandroidStatusCodes.LEAVING_HOME + && getStatusCode() != WorxLandroidStatusCodes.SEARCHING_ZONE) { + restoreZoneMeter = false; + setZoneMeters(zoneMeterRestoreValues); + sendCommand(new ZoneMeterCommand(getZoneMeters())); + } + + getSchedule().ifPresent(schedule -> { + setTimeExtension(schedule.timeExtension); + if (schedule.d != null) { + updateSchedules(0, schedule.d); + if (schedule.dd != null) { + updateSchedules(1, schedule.dd); + } + } + }); + + Cfg cfg = getPayloadCfg(); + if (multiZoneSupported()) { + for (int zoneIndex = 0; zoneIndex < cfg.multiZones.size(); zoneIndex++) { + setZoneMeter(zoneIndex, cfg.multiZones.get(zoneIndex)); + } + + for (int allocationIndex = 0; allocationIndex < cfg.multizoneAllocations.size(); allocationIndex++) { + setAllocation(allocationIndex, cfg.multizoneAllocations.get(allocationIndex)); + } + } + } + + private void updateSchedules(int scDSlot, List> d) { + Map planning = schedules.get(scDSlot); + EnumSet.allOf(WorxLandroidDayCodes.class).stream().forEach(dayCode -> { + List schedule = d.get(dayCode.code); + planning.put(dayCode, + new ScheduledDay(schedule.get(0), Integer.valueOf(schedule.get(1)), "1".equals(schedule.get(2)))); + }); + } + + public Optional getBattery() { + return Optional.ofNullable(getPayloadDat().battery); + } + + public Optional getRain() { + return Optional.ofNullable(getPayloadDat().rain); + } + + public double getAngle(Axis axis) { + return getPayloadDat().getAngle(axis); + } + + public Optional getStats() { + return Optional.ofNullable(getPayloadDat().st); + } + + public int getLastZone() { + return getAllocation(getPayloadDat().lastZone); + } + + public long getCurrentBladeTime() { + return getTotalBladeTime() - product.bladeWorkTimeReset; + } + + public long getTotalBladeTime() { + return lastStatus.payload.dat.st.bladeWorkTime; + } + + public int getCurrentChargeCycles() { + return product.batteryChargeCycles - product.batteryChargeCyclesReset; + } + + public int getTotalChargeCycles() { + return lastStatus.payload.dat.battery.chargeCycle; + } + + public WorxLandroidStatusCodes getStatusCode() { + return getPayloadDat().statusCode; + } + + public void setZoneTo(int zoneIndex) { + zoneMeterRestoreValues = getZoneMeters(); + restoreZoneMeter = true; + + int meter = getZoneMeter(zoneIndex); + for (int index = 0; index < 4; index++) { + setZoneMeter(index, meter); + } + } + + private void sendCommand(Object command) { + logger.debug("send command: {}", command); + mowerHandler.publishMessage(getMqttCommandIn(), command); + } + + public ZonedDateTime getLastUpdate() { + return getPayloadCfg().getDateTime(product.timeZone); + } + + public Optional getSchedule() { + return Optional.ofNullable(getPayloadCfg().sc); + } + + public Optional getOneTimeSchedule() { + return getSchedule().isPresent() ? Optional.ofNullable(getSchedule().get().ots) : Optional.empty(); + } +} diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/vo/ScheduledDay.java b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/vo/ScheduledDay.java new file mode 100644 index 0000000000000..b519f0079474c --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/java/org/openhab/binding/worxlandroid/internal/vo/ScheduledDay.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.worxlandroid.internal.vo; + +import java.time.LocalTime; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * {@link ScheduledDay} holds data of the schedule details for a given day + * + * @author Nils Billing - Initial contribution + */ +@NonNullByDefault +public class ScheduledDay { + public static final ScheduledDay BLANK = new ScheduledDay("00:00", 0, false); + + private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm"); + private static final int DEFAULT_DURATION = 15; + + private LocalTime startTime = LocalTime.MIN; + private boolean edgecut; + private int durationRestore = DEFAULT_DURATION; + private int duration; + + public ScheduledDay(String hhMm, int newDuration, boolean edgecut) { + this.startTime = LocalTime.parse(hhMm); + this.duration = newDuration; + this.edgecut = edgecut; + } + + public LocalTime getStartTime() { + return startTime; + } + + public void setStartTime(String hhMm) throws DateTimeParseException { + startTime = LocalTime.parse(hhMm); + } + + public void setStartTime(ZonedDateTime zdt) { + startTime = zdt.toLocalTime(); + } + + public int getDuration() { + return duration; + } + + public void setDuration(int newDuration) { + if (newDuration == 0 && duration > 0) { + durationRestore = duration; + } + + duration = newDuration; + } + + public boolean isEdgecut() { + return edgecut; + } + + public void setEdgecut(boolean edgecut) { + this.edgecut = edgecut; + } + + public boolean isEnabled() { + return duration != 0; + } + + public void setEnable(boolean newStatus) { + setDuration(newStatus && duration == 0 ? durationRestore : 0); + } + + public Object[] asArray() { + return new Object[] { startTime.format(TIME_FORMAT), duration, edgecut ? 1 : 0 }; + } +} diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.worxlandroid/src/main/resources/OH-INF/addon/addon.xml new file mode 100644 index 0000000000000..2845edd50de07 --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/resources/OH-INF/addon/addon.xml @@ -0,0 +1,11 @@ + + + + binding + WorxLandroid Binding + This is the binding for Worx Landroid. + cloud + + diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/resources/OH-INF/i18n/worxlandroid.properties b/bundles/org.openhab.binding.worxlandroid/src/main/resources/OH-INF/i18n/worxlandroid.properties new file mode 100644 index 0000000000000..460f932ed9dc3 --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/resources/OH-INF/i18n/worxlandroid.properties @@ -0,0 +1,355 @@ +# add-on + +addon.worxlandroid.name = WorxLandroid Binding +addon.worxlandroid.description = This is the binding for Worx Landroid. + +# thing types + +thing-type.worxlandroid.bridge.label = Bridge Worx Landroid API +thing-type.worxlandroid.bridge.description = Represents the API and handler for Worx Landroid. +thing-type.worxlandroid.mower.label = Landroid Mower +thing-type.worxlandroid.mower.description = Represents a Landroid Worx Mower +thing-type.worxlandroid.mower.group.friday.label = Friday Schedule +thing-type.worxlandroid.mower.group.friday2.label = Friday Slot 2 Schedule +thing-type.worxlandroid.mower.group.monday.label = Monday Schedule +thing-type.worxlandroid.mower.group.monday2.label = Monday Slot 2 Schedule +thing-type.worxlandroid.mower.group.saturday.label = Saturday Schedule +thing-type.worxlandroid.mower.group.saturday2.label = Saturday Slot 2 Schedule +thing-type.worxlandroid.mower.group.sunday.label = Sunday Schedule +thing-type.worxlandroid.mower.group.sunday2.label = Sunday Slot 2 Schedule +thing-type.worxlandroid.mower.group.thursday.label = Thursday Schedule +thing-type.worxlandroid.mower.group.thursday2.label = Thursday Slot 2 Schedule +thing-type.worxlandroid.mower.group.tuesday.label = Tuesday Schedule +thing-type.worxlandroid.mower.group.tuesday2.label = Tuesday Slot 2 Schedule +thing-type.worxlandroid.mower.group.wednesday.label = Wednesday Schedule +thing-type.worxlandroid.mower.group.wednesday2.label = Wednesday Slot 2 Schedule + +# thing types config + +thing-type.config.worxlandroid.bridge.password.label = Password +thing-type.config.worxlandroid.bridge.password.description = Password to access the Landroid WebAPI. +thing-type.config.worxlandroid.bridge.username.label = Username +thing-type.config.worxlandroid.bridge.username.description = Username to access the Landroid WebAPI. +thing-type.config.worxlandroid.mower.pollingInterval.label = Polling Interval +thing-type.config.worxlandroid.mower.pollingInterval.description = Interval for polling in seconds +thing-type.config.worxlandroid.mower.refreshStatusInterval.label = Refresh Status Interval +thing-type.config.worxlandroid.mower.refreshStatusInterval.description = Interval for refreshing mower status in seconds +thing-type.config.worxlandroid.mower.serialNumber.label = Serial Number +thing-type.config.worxlandroid.mower.serialNumber.description = Serial number of the mower + +# channel group types + +channel-group-type.worxlandroid.aws-group-type.label = Aws +channel-group-type.worxlandroid.aws-group-type.description = MQTT connection to AWS +channel-group-type.worxlandroid.aws-group-type.channel.connected.label = Connected +channel-group-type.worxlandroid.aws-group-type.channel.connected.description = Connection to AWS is alive +channel-group-type.worxlandroid.aws-group-type.channel.poll.label = Poll AWS +channel-group-type.worxlandroid.aws-group-type.channel.poll.description = Enables or disables polling Worx AWS +channel-group-type.worxlandroid.battery-group-type.label = Battery +channel-group-type.worxlandroid.battery-group-type.description = Battery channels of your mower +channel-group-type.worxlandroid.battery-group-type.channel.charge-cycles.label = Charge Cycles +channel-group-type.worxlandroid.battery-group-type.channel.charge-cycles-total.label = Total Charge Cycles +channel-group-type.worxlandroid.common-group-type.label = Common +channel-group-type.worxlandroid.common-group-type.description = Common channels of the mower +channel-group-type.worxlandroid.common-group-type.channel.enable.label = Mowing enabled +channel-group-type.worxlandroid.common-group-type.channel.online.label = Online +channel-group-type.worxlandroid.common-group-type.channel.online.description = Online status of the mower +channel-group-type.worxlandroid.common-group-type.channel.online-timestamp.label = Online Status Timestamp +channel-group-type.worxlandroid.config-group-type.label = Configuration +channel-group-type.worxlandroid.config-group-type.description = Configuration channels of your mower +channel-group-type.worxlandroid.config-group-type.channel.command.label = Command +channel-group-type.worxlandroid.day-sched-group-type.label = Daily Schedule +channel-group-type.worxlandroid.day-sched-group-type.description = Configuration schedule channels for each day +channel-group-type.worxlandroid.day-sched-group-type.channel.edgecut.label = Edgecut +channel-group-type.worxlandroid.day-sched-group-type.channel.enable.label = Active +channel-group-type.worxlandroid.day-sched-group-type.channel.enable.description = Defines if this day schedule is active or not +channel-group-type.worxlandroid.metrics-group-type.label = Metrics +channel-group-type.worxlandroid.metrics-group-type.description = Stat channels of your mower +channel-group-type.worxlandroid.metrics-group-type.channel.blade-time.label = Current Blade Time +channel-group-type.worxlandroid.metrics-group-type.channel.blade-time-total.label = Total Blade Time +channel-group-type.worxlandroid.metrics-group-type.channel.total-time.label = Total Time +channel-group-type.worxlandroid.multi-zones-group-type.label = Multi-Zone +channel-group-type.worxlandroid.multi-zones-group-type.description = Multi-Zones configuration of your mower +channel-group-type.worxlandroid.multi-zones-group-type.channel.allocation-0.label = Zone Allocation 1 +channel-group-type.worxlandroid.multi-zones-group-type.channel.allocation-1.label = Zone Allocation 2 +channel-group-type.worxlandroid.multi-zones-group-type.channel.allocation-2.label = Zone Allocation 3 +channel-group-type.worxlandroid.multi-zones-group-type.channel.allocation-3.label = Zone Allocation 4 +channel-group-type.worxlandroid.multi-zones-group-type.channel.allocation-4.label = Zone Allocation 5 +channel-group-type.worxlandroid.multi-zones-group-type.channel.allocation-5.label = Zone Allocation 6 +channel-group-type.worxlandroid.multi-zones-group-type.channel.allocation-6.label = Zone Allocation 7 +channel-group-type.worxlandroid.multi-zones-group-type.channel.allocation-7.label = Zone Allocation 8 +channel-group-type.worxlandroid.multi-zones-group-type.channel.allocation-8.label = Zone Allocation 9 +channel-group-type.worxlandroid.multi-zones-group-type.channel.allocation-9.label = Zone Allocation 10 +channel-group-type.worxlandroid.multi-zones-group-type.channel.enable.label = Multizone Enabled +channel-group-type.worxlandroid.multi-zones-group-type.channel.last-zone.label = Last Zone +channel-group-type.worxlandroid.multi-zones-group-type.channel.zone-1.label = Meters Zone 1 +channel-group-type.worxlandroid.multi-zones-group-type.channel.zone-2.label = Meters Zone 2 +channel-group-type.worxlandroid.multi-zones-group-type.channel.zone-3.label = Meters Zone 3 +channel-group-type.worxlandroid.multi-zones-group-type.channel.zone-4.label = Meters Zone 4 +channel-group-type.worxlandroid.orientation-group-type.label = Orientation +channel-group-type.worxlandroid.orientation-group-type.description = Orientation of your mower +channel-group-type.worxlandroid.orientation-group-type.channel.pitch.label = Pitch +channel-group-type.worxlandroid.orientation-group-type.channel.roll.label = Roll +channel-group-type.worxlandroid.orientation-group-type.channel.yaw.label = Yaw +channel-group-type.worxlandroid.ot-sched-group-type.label = One-Time Schedule +channel-group-type.worxlandroid.ot-sched-group-type.description = One time schedule configuration of your mower +channel-group-type.worxlandroid.ot-sched-group-type.channel.edgecut.label = Schedule Edgecut +channel-group-type.worxlandroid.rain-group-type.label = Rain +channel-group-type.worxlandroid.rain-group-type.description = Data rain channels of your mower +channel-group-type.worxlandroid.sched-group-type.label = Schedule +channel-group-type.worxlandroid.sched-group-type.description = Schedule channels configuration of your mower +channel-group-type.worxlandroid.sched-group-type.channel.next-start.label = Next Start +channel-group-type.worxlandroid.sched-group-type.channel.next-start.description = Next mowing start based on schedule +channel-group-type.worxlandroid.sched-group-type.channel.next-stop.label = Next Stop +channel-group-type.worxlandroid.sched-group-type.channel.next-stop.description = Next mowing stop based on schedule +channel-group-type.worxlandroid.wifi-group-type.label = Wi-Fi Information + +# channel types + +channel-type.worxlandroid.action-type.label = Action +channel-type.worxlandroid.action-type.description = Action channel for your mower +channel-type.worxlandroid.action-type.state.option.START = Start +channel-type.worxlandroid.action-type.state.option.STOP = Stop +channel-type.worxlandroid.action-type.state.option.HOME = Home +channel-type.worxlandroid.axis-type.label = Axis +channel-type.worxlandroid.battery-temp-type.label = Battery Temperature +channel-type.worxlandroid.battery-temp-type.description = Current temperature of the battery +channel-type.worxlandroid.charging-type.label = Battery Charging +channel-type.worxlandroid.distance-type.label = Total Distance +channel-type.worxlandroid.error-type.label = Error Code +channel-type.worxlandroid.error-type.state.option.UNKNOWN = Unknown +channel-type.worxlandroid.error-type.state.option.NO_ERR = No error +channel-type.worxlandroid.error-type.state.option.TRAPPED = Trapped +channel-type.worxlandroid.error-type.state.option.LIFTED = Lifted +channel-type.worxlandroid.error-type.state.option.WIRE_MISSING = Wire missing +channel-type.worxlandroid.error-type.state.option.OUTSIDE_WIRE = Outside wire +channel-type.worxlandroid.error-type.state.option.RAINING = Raining +channel-type.worxlandroid.error-type.state.option.CLOSE_DOOR_TO_MOW = Close door to mow +channel-type.worxlandroid.error-type.state.option.CLOSE_DOOR_TO_GO_HOME = Close door to go home +channel-type.worxlandroid.error-type.state.option.BLADE_MOTOR_BLOCKED = Blade motor blocked +channel-type.worxlandroid.error-type.state.option.WHEEL_MOTOR_BLOKED = Wheel motor blocked +channel-type.worxlandroid.error-type.state.option.TRAPPED_TIMEOUT = Trapped timeout +channel-type.worxlandroid.error-type.state.option.UPSIDE_DOWN = Upside down +channel-type.worxlandroid.error-type.state.option.BATTERY_LOW = Battery low +channel-type.worxlandroid.error-type.state.option.REVERSE_WIRE = Reverse wire +channel-type.worxlandroid.error-type.state.option.CHARGE_ERROR = Charge error +channel-type.worxlandroid.error-type.state.option.TIMEOUT_FINDING_HOME = Timeout finding home +channel-type.worxlandroid.error-type.state.option.MOWER_LOCKED = Mower locked +channel-type.worxlandroid.error-type.state.option.BATTERY_OVER_TEMPERATURE = Battery over temperature +channel-type.worxlandroid.error-type.state.option.MOWER_OUTSIDE_WIRE = Mower outside wire +channel-type.worxlandroid.lock-type.label = Lock mower +channel-type.worxlandroid.lock-type.description = Lock or unlock your mower. +channel-type.worxlandroid.metrics-duration.label = Time +channel-type.worxlandroid.number-ro.label = A Number +channel-type.worxlandroid.rain-counter.label = Rain Counter +channel-type.worxlandroid.rain-delay-type.label = Delay +channel-type.worxlandroid.rain-state-type.label = State +channel-type.worxlandroid.rssi.label = RSSI +channel-type.worxlandroid.rssi.description = Received signal strength indicator +channel-type.worxlandroid.schedule-duration-type.label = Duration +channel-type.worxlandroid.schedule-mode-type.label = Schedule Mode +channel-type.worxlandroid.schedule-mode-type.description = Sets Normal or Party mode +channel-type.worxlandroid.schedule-mode-type.state.option.NORMAL = Normal +channel-type.worxlandroid.schedule-mode-type.state.option.PARTY = Party +channel-type.worxlandroid.schedule-time.label = Start Time +channel-type.worxlandroid.schedule-time.description = Start time of the mowing on this day +channel-type.worxlandroid.schedule-time.state.pattern = %1$tH:%1$tM +channel-type.worxlandroid.status-type.label = Status Code +channel-type.worxlandroid.status-type.state.option.UNKNOWN = Unknown +channel-type.worxlandroid.status-type.state.option.IDLE = Idle +channel-type.worxlandroid.status-type.state.option.HOME = Home +channel-type.worxlandroid.status-type.state.option.START_SEQUENCE = Start sequence +channel-type.worxlandroid.status-type.state.option.LEAVING_HOME = Leaving home +channel-type.worxlandroid.status-type.state.option.FOLLOW_WIRE = Follow wire +channel-type.worxlandroid.status-type.state.option.SEARCHING_HOME = Searching home +channel-type.worxlandroid.status-type.state.option.SEARCHING_WIRE = Searching wire +channel-type.worxlandroid.status-type.state.option.MOWING = Mowing +channel-type.worxlandroid.status-type.state.option.LIFTED = Lifted +channel-type.worxlandroid.status-type.state.option.TRAPPED = Trapped +channel-type.worxlandroid.status-type.state.option.BLADE_BLOCKED = Blade blocked +channel-type.worxlandroid.status-type.state.option.DEBUG = Debug +channel-type.worxlandroid.status-type.state.option.REMOTE_CONTROL = Remote control +channel-type.worxlandroid.status-type.state.option.ESCAPE_FROM_OLM = Escape from OLM +channel-type.worxlandroid.status-type.state.option.GOING_HOME = Going home +channel-type.worxlandroid.status-type.state.option.ZONE_TRAINING = Zone training +channel-type.worxlandroid.status-type.state.option.BORDER_CUT = Border cut +channel-type.worxlandroid.status-type.state.option.SEARCHING_ZONE = Searching zone +channel-type.worxlandroid.status-type.state.option.PAUSE = Pause +channel-type.worxlandroid.status-type.state.option.MANUAL_STOP = Manual stop +channel-type.worxlandroid.switch-ro.label = Read Only Switch +channel-type.worxlandroid.switch-rw.label = Read Write Switch +channel-type.worxlandroid.time-extension-type.label = Schedule Time Extension +channel-type.worxlandroid.timestamp.label = Last Update +channel-type.worxlandroid.timestamp.description = Last device update +channel-type.worxlandroid.voltage-type.label = Battery Voltage +channel-type.worxlandroid.voltage-type.description = Battery voltage reported by the mower +channel-type.worxlandroid.zone-meter-type.label = Length of the zone +channel-type.worxlandroid.zone-type.label = Zone Number +channel-type.worxlandroid.zone-type.state.option.0 = Zone 1 +channel-type.worxlandroid.zone-type.state.option.1 = Zone 2 +channel-type.worxlandroid.zone-type.state.option.2 = Zone 3 +channel-type.worxlandroid.zone-type.state.option.3 = Zone 4 + +# channel group types + +channel-group-type.worxlandroid.battery-group-type.channel.charge-cycles-current.label = Current Charge Cycles +channel-group-type.worxlandroid.metrics-group-type.channel.blade-time-current.label = Current Blade Time + +# thing types config + +thing-type.config.worxlandroid.bridge.reconnectInterval.label = Reconnect Interval +thing-type.config.worxlandroid.bridge.reconnectInterval.description = Interval for reconnecting in seconds (after 600 seconds of inactivity, the connection is closed) + +# channel group types + +channel-group-type.worxlandroid.battery-group-type.channel.charge-cycle.label = Battery Charge Cycle Total +channel-group-type.worxlandroid.battery-group-type.channel.charge-cycle-current.label = Battery Charge Cycle Current +channel-group-type.worxlandroid.common-group-type.channel.poll.label = Poll AWS +channel-group-type.worxlandroid.common-group-type.channel.poll.description = Enables or disables polling Worx AWS +channel-group-type.worxlandroid.metrics-group-type.channel.current-blade-time.label = Current Blade Time +channel-group-type.worxlandroid.metrics-group-type.channel.total-blade-time.label = Total Blade Time +channel-group-type.worxlandroid.rain-group-type.channel.counter.label = Counter + +# channel types + +channel-type.worxlandroid.schedule-mode-type.state.option.1 = Normal +channel-type.worxlandroid.schedule-mode-type.state.option.2 = Party + +# thing types + +thing-type.worxlandroid.mower.group.cfgScFriday.label = Configuration schedule channels Friday +thing-type.worxlandroid.mower.group.cfgScFriday2.label = Configuration schedule channels Friday Slot 2 +thing-type.worxlandroid.mower.group.cfgScMonday.label = Configuration schedule channels Monday +thing-type.worxlandroid.mower.group.cfgScMonday2.label = Configuration schedule channels Monday Slot 2 +thing-type.worxlandroid.mower.group.cfgScSaturday.label = Configuration schedule channels Saturday +thing-type.worxlandroid.mower.group.cfgScSaturday2.label = Configuration schedule channels Saturday Slot 2 +thing-type.worxlandroid.mower.group.cfgScSunday.label = Configuration schedule channels Sunday +thing-type.worxlandroid.mower.group.cfgScSunday2.label = Configuration schedule channels Sunday Slot 2 +thing-type.worxlandroid.mower.group.cfgScThursday.label = Configuration schedule channels Thursday +thing-type.worxlandroid.mower.group.cfgScThursday2.label = Configuration schedule channels Thursday Slot 2 +thing-type.worxlandroid.mower.group.cfgScTuesday.label = Configuration schedule channels Tuesday +thing-type.worxlandroid.mower.group.cfgScTuesday2.label = Configuration schedule channels Tuesday Slot 2 +thing-type.worxlandroid.mower.group.cfgScWednesday.label = Configuration schedule channels Wednesday +thing-type.worxlandroid.mower.group.cfgScWednesday2.label = Configuration schedule channels Wednesday Slot 2 + +# thing types config + +thing-type.config.worxlandroid.mower.reconnectInterval.label = Reconnect Interval +thing-type.config.worxlandroid.mower.reconnectInterval.description = Interval for reconnecting in seconds (after 10 minutes / 600 seconds of inactivity, the connection is closed) +thing-type.config.worxlandroid.bridge.webapiPassword.label = Password +thing-type.config.worxlandroid.bridge.webapiPassword.description = Password to access the Landroid WebAPI. +thing-type.config.worxlandroid.bridge.webapiUsername.label = Username +thing-type.config.worxlandroid.bridge.webapiUsername.description = Username to access the Landroid WebAPI. + +# channel group types + +channel-group-type.worxlandroid.cfgCommon-type.label = Configuration common channels +channel-group-type.worxlandroid.cfgCommon-type.description = Configuration common channels of your mower +channel-group-type.worxlandroid.cfgMultiZones-type.label = Configuration multi zone channels +channel-group-type.worxlandroid.cfgMultiZones-type.description = Configuration multi zones of your mower +channel-group-type.worxlandroid.cfgMultiZones-type.channel.allocation0.label = Zone Allocation 1 +channel-group-type.worxlandroid.cfgMultiZones-type.channel.allocation1.label = Zone Allocation 2 +channel-group-type.worxlandroid.cfgMultiZones-type.channel.allocation2.label = Zone Allocation 3 +channel-group-type.worxlandroid.cfgMultiZones-type.channel.allocation3.label = Zone Allocation 4 +channel-group-type.worxlandroid.cfgMultiZones-type.channel.allocation4.label = Zone Allocation 5 +channel-group-type.worxlandroid.cfgMultiZones-type.channel.allocation5.label = Zone Allocation 6 +channel-group-type.worxlandroid.cfgMultiZones-type.channel.allocation6.label = Zone Allocation 7 +channel-group-type.worxlandroid.cfgMultiZones-type.channel.allocation7.label = Zone Allocation 8 +channel-group-type.worxlandroid.cfgMultiZones-type.channel.allocation8.label = Zone Allocation 9 +channel-group-type.worxlandroid.cfgMultiZones-type.channel.allocation9.label = Zone Allocation 10 +channel-group-type.worxlandroid.cfgMultiZones-type.channel.enable.label = Multizone enabled +channel-group-type.worxlandroid.cfgMultiZones-type.channel.zone1Meter.label = Meters Zone 1 +channel-group-type.worxlandroid.cfgMultiZones-type.channel.zone2Meter.label = Meters Zone 2 +channel-group-type.worxlandroid.cfgMultiZones-type.channel.zone3Meter.label = Meters Zone 3 +channel-group-type.worxlandroid.cfgMultiZones-type.channel.zone4Meter.label = Meters Zone 4 +channel-group-type.worxlandroid.cfgOneTimeSc-type.label = Configuration one time schedule channels +channel-group-type.worxlandroid.cfgOneTimeSc-type.description = Configuration one time schedule channels of your mower +channel-group-type.worxlandroid.cfgSc-type.label = Configuration schedule channels +channel-group-type.worxlandroid.cfgSc-type.description = Configuration schedule channels of your mower +channel-group-type.worxlandroid.cfgScDay-type.label = Configuration schedule channels for each day +channel-group-type.worxlandroid.cfgScDay-type.description = Configuration schedule channels for each day +channel-group-type.worxlandroid.common-type.label = Common channels +channel-group-type.worxlandroid.common-type.description = Common channels of your mower +channel-group-type.worxlandroid.common-type.channel.enable.label = Mowing enabled +channel-group-type.worxlandroid.datBattery-type.label = Data battery channels +channel-group-type.worxlandroid.datBattery-type.description = Data battery channels of your mower +channel-group-type.worxlandroid.datBattery-type.channel.batteryChargeCycle.label = Battery Charge Cycle Total +channel-group-type.worxlandroid.datBattery-type.channel.batteryChargeCycleCurrent.label = Battery Charge Cycle Current +channel-group-type.worxlandroid.datCommon-type.label = Data common channels +channel-group-type.worxlandroid.datCommon-type.description = Data common channels of your mower +channel-group-type.worxlandroid.datDmp-type.label = Data dmp channels +channel-group-type.worxlandroid.datDmp-type.description = Data dmp channels of your mower +channel-group-type.worxlandroid.datRain-type.label = Data rain channels +channel-group-type.worxlandroid.datRain-type.description = Data rain channels of your mower +channel-group-type.worxlandroid.datSt-type.label = Data st channels +channel-group-type.worxlandroid.datSt-type.description = Data st channels of your mower +channel-group-type.worxlandroid.datSt-type.channel.currentBladeTime.label = Current Blade Time +channel-group-type.worxlandroid.datSt-type.channel.totalBladeTime.label = Total Blade Time +channel-group-type.worxlandroid.datSt-type.channel.totalTime.label = Total Time + +# channel types + +channel-type.worxlandroid.chAction.label = Action +channel-type.worxlandroid.chAction.description = Action channel for your mower +channel-type.worxlandroid.chAction.state.option.START = Start +channel-type.worxlandroid.chAction.state.option.STOP = Stop +channel-type.worxlandroid.chAction.state.option.HOME = Home +channel-type.worxlandroid.chAllocation.label = Zone Allocation +channel-type.worxlandroid.chAllocation.state.option.0 = Zone 1 +channel-type.worxlandroid.chAllocation.state.option.1 = Zone 2 +channel-type.worxlandroid.chAllocation.state.option.2 = Zone 3 +channel-type.worxlandroid.chAllocation.state.option.3 = Zone 4 +channel-type.worxlandroid.chBatteryChargeCycle.label = Battery Charge Cycle +channel-type.worxlandroid.chBatteryCharging.label = Battery Charging +channel-type.worxlandroid.chBatteryLevel.label = Battery Level +channel-type.worxlandroid.chBatteryTemperature.label = Battery Temperature +channel-type.worxlandroid.chBatteryVoltage.label = Battery Voltage +channel-type.worxlandroid.chCommand.label = Command +channel-type.worxlandroid.chEnable.label = Activation / Deactivation +channel-type.worxlandroid.chErrorCode.label = Error Code +channel-type.worxlandroid.chErrorDescription.label = Error Description +channel-type.worxlandroid.chFirmware.label = Firmware +channel-type.worxlandroid.chId.label = Id +channel-type.worxlandroid.chLanguage.label = Language +channel-type.worxlandroid.chLastUpdate.label = Last Update +channel-type.worxlandroid.chLastUpdateOnlineStatus.label = Last Update Online Status +channel-type.worxlandroid.chLastZone.label = Last Zone +channel-type.worxlandroid.chLastZone.state.option.0 = Zone 1 +channel-type.worxlandroid.chLastZone.state.option.1 = Zone 2 +channel-type.worxlandroid.chLastZone.state.option.2 = Zone 3 +channel-type.worxlandroid.chLastZone.state.option.3 = Zone 4 +channel-type.worxlandroid.chLock.label = Lock mower +channel-type.worxlandroid.chLock.description = Lock or unlock your mower. +channel-type.worxlandroid.chMacAdress.label = MacAdress +channel-type.worxlandroid.chOnline.label = Online +channel-type.worxlandroid.chPitch.label = Pitch +channel-type.worxlandroid.chPoll.label = Poll Worx AWS +channel-type.worxlandroid.chPoll.description = Poll Worx AWS +channel-type.worxlandroid.chRainCounter.label = Rain counter +channel-type.worxlandroid.chRainDelay.label = Rain Delay +channel-type.worxlandroid.chRainState.label = Rain state +channel-type.worxlandroid.chRoll.label = Roll +channel-type.worxlandroid.chScheduleDuration.label = Schedule Duration +channel-type.worxlandroid.chScheduleEdgecut.label = Schedule Edgecut +channel-type.worxlandroid.chScheduleMode.label = Schedule Mode (Party) +channel-type.worxlandroid.chScheduleMode.state.option.1 = Normal +channel-type.worxlandroid.chScheduleMode.state.option.2 = Party +channel-type.worxlandroid.chScheduleStartHour.label = Schedule Start Hour +channel-type.worxlandroid.chScheduleStartMinutes.label = Schedule Start Minutes +channel-type.worxlandroid.chScheduleTimeExtension.label = Schedule Time Extension +channel-type.worxlandroid.chStatusCode.label = Status Code +channel-type.worxlandroid.chStatusDescription.label = Status Description +channel-type.worxlandroid.chTimeMinutes.label = Time +channel-type.worxlandroid.chTotalDistance.label = Total Distance +channel-type.worxlandroid.chWifiQuality.label = Wifi Quality +channel-type.worxlandroid.chYaw.label = Yaw +channel-type.worxlandroid.chZoneMeter.label = Meters of zone + +# error messages + +conf-error-no-username = Cannot connect to Landroid bridge as no username is configured +conf-error-no-password = Cannot connect to Landroid bridge as no password is configured +conf-error-no-serial = No serial number configured for this mower +oauth-connection-error = Error getting access token +oauth-refresh-error = To many token refresh failed +oauth-connection-delayed = Unable to connect to Worx Api (403) - will retry later diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/resources/OH-INF/thing/bridge.xml b/bundles/org.openhab.binding.worxlandroid/src/main/resources/OH-INF/thing/bridge.xml new file mode 100644 index 0000000000000..829cbc940e9e3 --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/resources/OH-INF/thing/bridge.xml @@ -0,0 +1,22 @@ + + + + + Represents the API and handler for Worx Landroid. + + + + Username to access the Landroid WebAPI. + true + + + password + + Password to access the Landroid WebAPI. + true + + + + diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/resources/OH-INF/thing/channels.xml b/bundles/org.openhab.binding.worxlandroid/src/main/resources/OH-INF/thing/channels.xml new file mode 100644 index 0000000000000..9f5176191e27f --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/resources/OH-INF/thing/channels.xml @@ -0,0 +1,241 @@ + + + + + DateTime + + Last device update + Time + + + + + Number:Temperature + + Current temperature of the battery + Temperature + + + + + Number:ElectricPotential + + Battery voltage reported by the mower + Energy + + + + + Number:Angle + + Incline + + + + + Number:Length + + oh:worxlandroid:distance + + + + + Number:Time + + time + + + + + String + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + String + + Error + + + + + + + + + + + + + + + + + + + + + + + + + + + + Number:Dimensionless + + + + + + + String + + Sets Normal or Party mode + + + + + + + + + + + Number:Time + + + + + + Number:Length + + oh:worxlandroid:distance + + + + + Number + + oh:worxlandroid:zones + + + + + + + + + + + + String + + Action channel for your mower + + + + + + + + + + + Switch + + + + + Switch + + + + + + Switch + + oh:worxlandroid:charging + + + + + Switch + + Lock or unlock your mower. + oh:worxlandroid:lock + + + + Number + + oh:worxlandroid:counter + + + + + DateTime + + Start time of the mowing on this day + Time + + + + + Number:Time + + Time + + + + + Number:Time + + Time + + + + + Switch + + oh:worxlandroid:rain + + + + + Number:Power + + Received signal strength indicator + QualityOfService + + + + diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/resources/OH-INF/thing/groups.xml b/bundles/org.openhab.binding.worxlandroid/src/main/resources/OH-INF/thing/groups.xml new file mode 100644 index 0000000000000..7482e8e62b088 --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/resources/OH-INF/thing/groups.xml @@ -0,0 +1,222 @@ + + + + + + Common channels of the mower + + + + + + Online status of the mower + + + + + + + + + + + + + + + MQTT connection to AWS + + + + Enables or disables polling Worx AWS + + + + Connection to AWS is alive + + + + + + + Configuration channels of your mower + + + + + + + + + + + Multi-Zones configuration of your mower + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Battery channels of your mower + + + + + + + + + + + + + + + + + Orientation of your mower + + + + + + + + + + + + + + + + Stat channels of your mower + + + + + + + + + + + + + + + + + Schedule channels configuration of your mower + + + + + + Next mowing start based on schedule + + + + Next mowing stop based on schedule + + + + + + + One time schedule configuration of your mower + + + + + + + + + + + + Configuration schedule channels for each day + + + + Defines if this day schedule is active or not + + + + + + + + + + + + Data rain channels of your mower + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/resources/OH-INF/thing/mower.xml b/bundles/org.openhab.binding.worxlandroid/src/main/resources/OH-INF/thing/mower.xml new file mode 100644 index 0000000000000..4298c8782e43f --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/resources/OH-INF/thing/mower.xml @@ -0,0 +1,99 @@ + + + + + + + + + + Represents a Landroid Worx Mower + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + serialNumber + + + + + Serial number of the mower + true + + + + + Interval for refreshing mower status in seconds + 3600 + true + true + + + + + Interval for polling in seconds + 1200 + true + true + + + + + + diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/charging-off.svg b/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/charging-off.svg new file mode 100644 index 0000000000000..92dc0a04f0121 --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/charging-off.svg @@ -0,0 +1,29 @@ + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/charging-on.svg b/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/charging-on.svg new file mode 100644 index 0000000000000..a8fceadf37bc3 --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/charging-on.svg @@ -0,0 +1,31 @@ + + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/charging.svg b/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/charging.svg new file mode 100644 index 0000000000000..9df46be9f21d1 --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/charging.svg @@ -0,0 +1,29 @@ + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/counter.svg b/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/counter.svg new file mode 100644 index 0000000000000..2e93695598200 --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/counter.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/distance-0.svg b/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/distance-0.svg new file mode 100644 index 0000000000000..758f28c2abf64 --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/distance-0.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/distance.svg b/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/distance.svg new file mode 100644 index 0000000000000..df770b90b1527 --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/distance.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/landroid.png b/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/landroid.png new file mode 100644 index 0000000000000..fca59740d1b82 Binary files /dev/null and b/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/landroid.png differ diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/lawnmower.svg b/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/lawnmower.svg new file mode 100644 index 0000000000000..2a190c6033e63 --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/lawnmower.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/lock-off.svg b/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/lock-off.svg new file mode 100644 index 0000000000000..7db9c34984440 --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/lock-off.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/lock-on.svg b/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/lock-on.svg new file mode 100644 index 0000000000000..06a6b4487c109 --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/lock-on.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/lock.svg b/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/lock.svg new file mode 100644 index 0000000000000..06a6b4487c109 --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/lock.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/rain-off.svg b/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/rain-off.svg new file mode 100644 index 0000000000000..cc1ad06ac4c1d --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/rain-off.svg @@ -0,0 +1,79 @@ + + + + 10C30B00-D619-4361-8391-7C1E95266FCA + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/rain-on.svg b/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/rain-on.svg new file mode 100644 index 0000000000000..0a59be11e39c2 --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/rain-on.svg @@ -0,0 +1,65 @@ + + + + 7D4327F6-13C6-4F8C-8EA0-F2CACE56A545 + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/rain.svg b/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/rain.svg new file mode 100644 index 0000000000000..91d868a6fb22d --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/rain.svg @@ -0,0 +1,374 @@ + + + + + + image/svg+xml + + + + + + + 10C30B00-D619-4361-8391-7C1E95266FCA + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/refresh.svg b/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/refresh.svg new file mode 100644 index 0000000000000..dff1894e2a57a --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/refresh.svg @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/zones-0.svg b/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/zones-0.svg new file mode 100644 index 0000000000000..fca8332aecb16 --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/zones-0.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + 1 + Enter your text here + 2 + 3 + 4 + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/zones-1.svg b/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/zones-1.svg new file mode 100644 index 0000000000000..dc2d57709be01 --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/zones-1.svg @@ -0,0 +1,15 @@ + + + + + + + + 1 + Enter your text here + 2 + 3 + 4 + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/zones-2.svg b/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/zones-2.svg new file mode 100644 index 0000000000000..35ac13a11831b --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/zones-2.svg @@ -0,0 +1,15 @@ + + + + + + + + 1 + Enter your text here + 2 + 3 + 4 + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/zones-3.svg b/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/zones-3.svg new file mode 100644 index 0000000000000..ad20d6c5b1ef7 --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/zones-3.svg @@ -0,0 +1,15 @@ + + + + + + + + 1 + Enter your text here + 2 + 3 + 4 + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/zones.svg b/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/zones.svg new file mode 100644 index 0000000000000..cfa13d1bee7f8 --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/resources/icon/zones.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + 1 + Enter your text here + 2 + 3 + 4 + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/resources/landroid_error_en.map b/bundles/org.openhab.binding.worxlandroid/src/main/resources/landroid_error_en.map new file mode 100644 index 0000000000000..4d1db31d37148 --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/resources/landroid_error_en.map @@ -0,0 +1,20 @@ +UNKNOWN=unknown +NO_ERR=no error +TRAPPED=trapped +LIFTED=lifted +WIRE_MISSING=wire missing +OUTSIDE_WIRE=outside wire +RAINING=raining +CLOSE_DOOR_TO_MOW=close door to mow +CLOSE_DOOR_TO_GO_HOME=close door to go home +BLADE_MOTOR_BLOCKED=blade motor blocked +WHEEL_MOTOR_BLOCKED=wheel motor blocked +TRAPPED_TIMEOUT=trapped timeout +UPSIDE_DOWN=upside down +BATTERY_LOW=battery low +REVERSE_WIRE=reverse wire +CHARGE_ERROR=charge errir +TIMEOUT_FINDING_HOME=timeout finding home +MOWER_LOCKED=mower locked +BATTERY_OVER_TEMPERATURE=batter over temperature +MOWER_OUTSIDE_WIRE=mower outside wire diff --git a/bundles/org.openhab.binding.worxlandroid/src/main/resources/landroid_status_en.map b/bundles/org.openhab.binding.worxlandroid/src/main/resources/landroid_status_en.map new file mode 100644 index 0000000000000..abfedb66c1b2b --- /dev/null +++ b/bundles/org.openhab.binding.worxlandroid/src/main/resources/landroid_status_en.map @@ -0,0 +1,20 @@ +UNKNOWN=unknown +IDLE=idle +HOME=home +START_SEQUENCE=start sequence +LEAVING_HOME=leaving home +FOLLOW_WIRE=follow wire +SEARCHING_HOME=searching home +SEARCHING_WIRE=searching wire +MOWING=mowing +LIFTED=lifted +TRAPPED=trapped +BLADE_BLOCKED=blade blocked +DEBUG=debug +REMOTE_CONTROL=remote control +GOING_HOME=going home +ZONE_TRAINING=zone training +BORDER_CUT=border cut +SEARCHING_ZONE=searching zone +PAUSE=pause +MANUAL_STOP=manual stop diff --git a/bundles/pom.xml b/bundles/pom.xml index fd25a11558232..88978c2de8537 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -475,6 +475,7 @@ org.openhab.binding.wlanthermo org.openhab.binding.wled org.openhab.binding.wolfsmartset + org.openhab.binding.worxlandroid org.openhab.binding.wundergroundupdatereceiver org.openhab.binding.x org.openhab.binding.xmltv