[automower] Status update via Husqvarna WebSocket API (#18630)

* initial checkin: working WSS communication

Signed-off-by: Michael Weger <weger.michael@gmx.net>
This commit is contained in:
MikeTheTux
2025-07-27 15:02:17 +02:00
committed by GitHub
parent 0d1dcd6133
commit 5acfef3f97
17 changed files with 1964 additions and 978 deletions
+95 -99
View File
@@ -1,7 +1,7 @@
# Automower Binding
This is the binding for [Husqvarna Automower® robotic lawn mowers](https://www.husqvarna.com/uk/products/robotic-lawn-mowers/).
This binding allows you to integrate, view and control Automower® lawn mowers in the openHAB environment.
This binding allows you to integrate, view and control Husqvarna Automower® lawn mowers in the openHAB environment.
## Supported Things
@@ -21,62 +21,73 @@ Once the bridge is created and configured, openHAB will automatically discover a
- appKey (mandatory): The Application Key is required to communicate with the Automower® Connect API. It can be obtained by registering an Application on [the Husqvarna Website](https://developer.husqvarnagroup.cloud/). This application also needs to be connected to the ["Authentication API" and the "Automower® Connect API"](https://developer.husqvarnagroup.cloud/docs/getting-started)
- appSecret (mandatory): The Application Secret is required to communicate with the Automower® Connect API. It can be obtained by registering an Application on [the Husqvarna Website](https://developer.husqvarnagroup.cloud/).
- pollingInterval (optional): How often the bridge state should be queried in seconds. Default is 1h (3600s)
- pollingInterval (optional): How often the current Automower® states should be polled in seconds via REST API. Default is 5min (300s)
Keep in mind that the status of the bridge should not be queried too often.
According to the Husqvarna documentation not more than 10000 requests per month / 1 request per second and application key are allowed.
With the default value of 1h this would mean ~720 requests per month for the bridge state
Keep in mind that the REST API should not be queried too frequently.
According to Husqvarna's guidelines, each application key is limited to 10.000 requests per month and 1 request per second.
With the default polling interval of 5min, the bridge will make approximately 8.640 requests per month.
As the states are polled from the `bridge`, the number does not scale with the number of `automower`.
In addition to periodic polling, the binding also receives event-triggered notifications whenever there are changes to the Automower®'s status, position, settings, or messages.
`automower:`
- mowerId (mandatory): The Id of an Automower® as used by the Automower® Connect Api to identify a mower. This is automatically filled when the thing is discovered
- pollingInterval (optional): How often the current Automower® state should be polled in seconds. Default is 10min (600s)
Keep in mind that the status of the Automower® should not be queried too often.
According to the Husqvarna documentation not more than 10000 requests per month / 1 request per second and application key are allowed.
With the default value of 10min this would mean ~4300 requests per month per single Automower®
- mowerId (mandatory): The Id of an Automower® as used by the Automower® Connect API to identify a Automower®. This is automatically filled when the thing is discovered
- mowerZoneId (optional): Time zone of the Automower® (e.g. Europe/Berlin). Default is the time zone of the system
## Channels
### Status Channels
These channels represent the Automower® status.
| channel | type | access mode | description | advanced |
|---------------------------------------|----------------------|-----|----------------------------------------------------------------------------------------------------------------|-------|
| status#name | String | R | The name of the Automower® | false |
| status#name | String | R | The name of the mower | false |
| status#mode | String | R | The current mode (MAIN_AREA, SECONDARY_AREA, HOME, DEMO, UNKNOWN) | false |
| status#activity | String | R | The current activity (UNKNOWN, NOT_APPLICABLE, MOWING, GOING_HOME, CHARGING, LEAVING, PARKED_IN_CS, STOPPED_IN_GARDEN) | false |
| status#inactive-reason | String | R | The current reason for being inactive (NONE, PLANNING, SEARCHING_FOR_SATELLITES) | false |
| status#state | String | R | The current state (UNKNOWN, NOT_APPLICABLE, PAUSED, IN_OPERATION, WAIT_UPDATING, WAIT_POWER_UP, RESTRICTED_NONE, RESTRICTED_WEEK_SCHEDULE, RESTRICTED_PARK_OVERRIDE, RESTRICTED_SENSOR, RESTRICTED_DAILY_LIMIT, RESTRICTED_FOTA, RESTRICTED_FROST, RESTRICTED_ALL_WORK_AREAS_COMPLETED, RESTRICTED_EXTERNAL, OFF, STOPPED, ERROR, FATAL_ERROR, ERROR_AT_POWER_UP) | false |
| status#work-area-id<sup id="a1">[1](#f1)</sup> | Number | R | Id of the active work area | true |
| status#work-area<sup id="a1">[1](#f1)</sup> | String | R | Name of the active work area | false |
| status#last-update | DateTime | R | The time when the Automower® updated its states | false |
| status#work-area-id<sup id="a1">[1](#f1)</sup> | Number | R | Id of the active Work Area | true |
| status#work-area<sup id="a1">[1](#f1)</sup> | String | R | Name of the active Work Area | false |
| status#last-update | DateTime | R | The time when the mower sent the last update | false |
| status#last-poll-update | DateTime | R | The time when the binding polled the last update from the cloud | true |
| status#poll-update | Switch | R | Poll Automower® status update from the cloud | true |
| status#poll-update | Switch | R/W | Poll mower status update from the cloud (`sendCommand(ON)`) | true |
| status#battery | Number:Dimensionless | R | The battery state of charge in percent | false |
| status#error-code | Number | R/W | The current error code. `sendCommand(0)` to confirm current non fatal error | true |
| status#error-message | String | R | The current error message | false |
| status#error-timestamp | DateTime | R | The timestamp when the current error occurred | false |
| status#error-confirmable<sup id="a1">[1](#f1)</sup> | Switch | R | If the mower has an Error Code this attribute states if the error is confirmable | true |
| status#next-start | DateTime | R | The time for the next auto start. If the mower is charging then the value is the estimated time when it will be leaving the charging station. If the mower is about to start now, the value is NULL | false |
| status#override-action | String | R | The action that overrides current planner operation | true |
| status#restricted-reason | String | R | A reason that restrics current planner operation (NONE, WEEK_SCHEDULE, PARK_OVERRIDE, SENSOR, DAILY_LIMIT, FOTA, FROST, ALL_WORK_AREAS_COMPLETED, EXTERNAL) | false |
| status#external-reason | String | R | An external reason set by i.e. IFTTT, Google Assistant or Amazon Alexa that restrics current planner operation | true |
| status#error-confirmable<sup id="a1">[1](#f1)</sup> | Switch | R | If the mower has an error, this attribute states if the error is confirmable | true |
| status#next-start | DateTime | R | The time for the next scheduled start. If the mower is charging then the value is the estimated time when it will leave the charging station. If the mower is about to start now, the value is NULL | false |
| status#override-action | String | R | The action that overrides the current planner operation | true |
| status#restricted-reason | String | R | The reason that restrics the current planner operation (NONE, WEEK_SCHEDULE, PARK_OVERRIDE, SENSOR, DAILY_LIMIT, FOTA, FROST, ALL_WORK_AREAS_COMPLETED, EXTERNAL) | false |
| status#external-reason | String | R | An external reason set by i.e. Google Assistant or Amazon Alexa that restrics the current planner operation | true |
| status#position | Location | R | Last GPS Position of the mower | false |
### Settings Channels
These channels hold Automower® settings.
| channel | type | access mode | description | advanced |
|---------------------------------------------------|--------|-------------|-------------------------------------------------------------------------|----------|
| setting#cutting-height | Number | R/W | Prescaled cutting height, Range: 1-9 | false |
| setting#headlight-mode<sup id="a1">[1](#f1)</sup> | String | R/W | Headlight Mode (ALWAYS_ON, ALWAYS_OFF, EVENING_ONLY, EVENING_AND_NIGHT) | false |
The absolute cutting height can be calculated from the prescaled cutting height using the following formula:
`cuttingHeightInCM = round((minCuttingHeight + ((maxCuttingHeight - minCuttingHeight) * (setting#cutting-height - 1) / 8)) * 2) / 2`
### Statistics Channels
These channels hold different Automower® statistics.
| channel | type | access mode | description | advanced |
|-------------------------------------|----------------------|-------------|---------------------------------------------------------------------------------------------|----------|
| statistic#cutting-blade-usage-time | Number:Time | R/W | The time since the last reset of the cutting blade usage counter. `sendCommand(0)` to reset | false |
| statistic#down-time | Number:Time | R | The time the mower has been disconnected from the cloud | true |
| statistic#number-of-charging-cycles | Number | R | Number of charging cycles | false |
| statistic#number-of-collisions | Number | R | The total number of collisions | false |
| statistic#number-of-collisions | Number | R | Total number of collisions | false |
| statistic#total-charging-time | Number:Time | R | Total charging time | false |
| statistic#total-cutting-time | Number:Time | R | Total Cutting Time | false |
| statistic#total-cutting-percent | Number:Dimensionless | R | Total cutting time in percent | false |
@@ -106,19 +117,6 @@ These channels hold the different Calendar Task configurations.
\<x\> ... 01-#calendartasks
### Position Channels
These channels hold the last 50 GPS positions recorded by the Automower®, thus describing the path it followed.
Position 01 is the latest recorded position, the other positions are pushed back, thus removing the previous position 50 from the list because it is replaced by the previous position 49.
Channel `last-position` is always identical with channel `position01` and thus provides more convenient access if only the latest GPS information is required by the user.
| channel | type | access mode | description | advanced |
|-----------------------------------------------|----------|-------------|---------------------------------------------------------|----------|
| position#last<sup id="a1">[1](#f1)</sup> | Location | R | Last GPS Position (identical with positions#position01) | false |
| position#\<x\>-pos<sup id="a1">[1](#f1)</sup> | Location | R | GPS Position \<x\> | true |
\<x\> ... 01-50
### Stayout Zones Channels
These channels hold the different Stayout Zone configurations.
@@ -126,9 +124,9 @@ These channels hold the different Stayout Zone configurations.
| channel | type | access mode | description | advanced |
|-----------------------------------------------------------|--------|-------------|------------------------------------------------------------------------------------------------------------------------------------|----------|
| stayoutzone#dirty<sup id="a1">[1](#f1)</sup> | Switch | R | If the stay-out zones are synchronized with the Husqvarna cloud. If the map is dirty you can not enable or disable a stay-out zone | true |
| stayoutzone#\<x\>-zone-id<sup id="a1">[1](#f1)</sup> | String | R | Id of the Stayout zone | true |
| stayoutzone#\<x\>-zone-name<sup id="a1">[1](#f1)</sup> | String | R | The name of the Stayout zone | true |
| stayoutzone#\<x\>-zone-enabled<sup id="a1">[1](#f1)</sup> | Switch | R/W | If the Stayout zone is enabled, the Automower® will not access the zone | true |
| stayoutzone#\<x\>-zone-id<sup id="a1">[1](#f1)</sup> | String | R | Id of the stay-out zone | true |
| stayoutzone#\<x\>-zone-name<sup id="a1">[1](#f1)</sup> | String | R | The name of the stay-out zone | true |
| stayoutzone#\<x\>-zone-enabled<sup id="a1">[1](#f1)</sup> | Switch | R/W | If the stay-out zone is enabled, the mower will not access the zone | true |
\<x\> ... 01-#stayoutzones
@@ -139,69 +137,68 @@ These channels hold the different Work Area configurations.
| channel | type | access mode | description | advanced |
|-----------------------------------------------------------------------------------------------------------|-----------------------|-------------|-----------------------------------------------------|----------|
| workarea#\<x\>-area-id<sup id="a1">[1](#f1)</sup> | Number | R | Id of the Work Area | false |
| workarea#\<x\>-area-name<sup id="a1">[1](#f1)</sup> | String | R | Name of the work area | false |
| workarea#\<x\>-area-cutting-height<sup id="a1">[1](#f1)</sup> | Number:Dimensionless | R/W | Cutting height in percent. 0-100 | false |
| workarea#\<x\>-area-enabled<sup id="a1">[1](#f1)</sup> | Switch | R/W | If the work area is enabled or disabled | false |
| workarea#\<x\>-area-progress<sup id="a1">[1](#f1)</sup><sup>,</sup><sup id="a2">[2](#f2)</sup> | Number | R | The progress on a work area | true |
| workarea#\<x\>-area-last-time-completed<sup id="a1">[1](#f1)</sup><sup>,</sup><sup id="a2">[2](#f2)</sup> | DateTime | R | Timestamp when the work area was last completed | true |
| workarea#\<x\>-area-name<sup id="a1">[1](#f1)</sup> | String | R | Name of the Work Area | false |
| workarea#\<x\>-area-cutting-height<sup id="a1">[1](#f1)</sup> | Number:Dimensionless | R/W | Cutting height of the Work Area in percent. 0-100 | false |
| workarea#\<x\>-area-enabled<sup id="a1">[1](#f1)</sup> | Switch | R/W | If the Work Area is enabled or disabled | false |
| workarea#\<x\>-area-progress<sup id="a1">[1](#f1)</sup><sup>,</sup><sup id="a2">[2](#f2)</sup> | Number | R | The progress on a Work Area | true |
| workarea#\<x\>-area-last-time-completed<sup id="a1">[1](#f1)</sup><sup>,</sup><sup id="a2">[2](#f2)</sup> | DateTime | R | Timestamp when the Work Area was last completed | true |
\<x\> ... 01-#workareas
### Messages
These channels hold the last 50 messages recorded by the Automower®.
Message 01 is the latest recorded message, the other messages are pushed back, thus removing the previous message 50 from the list because it is replaced by the previous message 49.
These channels hold the last message recorded by the Automower®.
| channel | type | access mode | description | advanced |
|-------------------------|----------|-------------|------------------------------------------|----------|
| message#\<x\>-time | DateTime | R | Timestamp when the event occurred | true |
| message#\<x\>-code | Number | R | (Error) code of the event | true |
| message#\<x\>-text | String | R | The message | true |
| message#\<x\>-severity | String | R | The severity of the event | true |
| message#\<x\>-position | Location | R | GPS Position of the event (if available) | true |
\<x\> ... 01-50
| channel | type | access mode | description | advanced |
|-----------------------|----------|-------------|-----------------------------------------------|----------|
| message#msg-timestamp | DateTime | R | The time when the last error occurred | true |
| message#msg-code | Number | R | The last error code | true |
| message#msg-text | String | R | The last error message | true |
| message#msg-severity | String | R | The severity of the last error | true |
| message#msg-position | Location | R | GPS position of the last event (if available) | true |
### Command Channels
Command channels that trigger actions.
| channel | type | access mode | description | advanced |
|-----------------------------------|----------|-------------|------------------------------------------|----------|
| command#start | Number | W | Start the Automower® for a duration | false |
| command#resume_schedule | Switch | W | Resume the Automower® schedule | false |
| command#pause | Switch | W | Pause the Automower® | false |
| command#park | Number | W | Park the Automower® for a duration | false |
| command#park_until_next_schedule | Switch | W | Park the Automower® until next schedule | false |
| command#park_until_further_notice | Switch | W | Park the Automower® until further notice | false |
| channel | type | access mode | description | advanced |
|-----------------------------------|----------|-------------|--------------------------------------------------------------------------------------------------------|----------|
| command#start | Number | W | Start the mower for the given duration, overriding the schedule | false |
| command#start_in_workarea | Number | W | Start the mower in the given Work Area, overriding the schedule. The mower will continue forever | false |
| command#resume_schedule | Switch | W | Resume the schedule of the mower | false |
| command#pause | Switch | W | Pause the mower at the current location until manual resume | false |
| command#park | Number | W | Park the mower for the given duration, overriding the schedule | false |
| command#park_until_next_schedule | Switch | W | Park the mower, fully charge it and start afterwards according to the schedule | false |
| command#park_until_further_notice | Switch | W | Park the mower until it is started again by the start action/command or the schedule gets resumed | false |
## Actions
The following actions are available for `automower` things:
| action name | arguments | description |
|----------------------------|------------------|------------------------------------------------------------------------------------------------|
| start | `duration (int)` | Start the Automower® for the given duration (minutes), overriding the schedule |
| pause | - | Pause the Automower® wherever it is currently located |
| parkUntilNextSchedule | - | Park the Automower®, fully charges it and starts afterwards according to the schedule |
| parkUntilFurtherNotice | - | Park the Automower® until it is started again by the start action or the schedule gets resumed |
| park | `duration (int)` | Park the Automower® for the given duration (minutes), overriding the schedule |
| resumeSchedule | - | Resume the schedule for the Automower® |
| confirmError | - | Confirm current non fatal error |
| resetCuttingBladeUsageTime | - | Reset the cutting blade usage time |
| setSettings | `byte cuttingHeight`<br/>`String headlightMode` | Update Automower® settings |
| setWorkArea | `long workAreaId`<br/>`boolean enable`<br/>`byte cuttingHeight` | Update work area settings |
| setStayOutZone | `String zoneId`<br/>`boolean enable` | Enable or disable stay-out zone |
| setCalendarTask | `Long workAreaId` (optional, set to `null` if the mower doesn't support work areas)<br/>`short[] start`<br/>`short[] duration`<br/>`boolean[] monday`<br/>`boolean[] tuesday`<br/>`boolean[] wednesday`<br/>`boolean[] thursday`<br/>`boolean[] friday`<br/>`boolean[] saturday`<br/>`boolean[] sunday` | Update calendar task settings. Parameter are an array for all calendar tasks (per work area) |
| poll | - | Poll Automower® status update from the cloud |
| action name | arguments | description |
|----------------------------|-------------------|--------------------------------------------------------------------------------------------------------|
| start | `duration (long)` | Start the mower for the given duration (minutes), overriding the schedule |
| startInWorkArea | `workAreaId (long)`<br/>`duration (long)` | Start the mower in the given Work Area for the given duration (minutes), overriding the schedule. If duration is skipped the mower will continue forever |
| pause | - | Pause the mower at the current location until manual resume |
| park | `duration (long)` | Park the mower for the given duration (minutes), overriding the schedule |
| parkUntilNextSchedule | - | Park the mower, fully charge it and start afterwards according to the schedule |
| parkUntilFurtherNotice | - | Park the mower until it is started again by the start action/command or the schedule gets resumed |
| resumeSchedule | - | Resume the schedule of the mower |
| confirmError | - | Confirm current non fatal error |
| resetCuttingBladeUsageTime | - | Reset the cutting blade usage time |
| setSettings | `byte cuttingHeight`<br/>`String headlightMode` | Update mower settings |
| setWorkArea | `long workAreaId`<br/>`boolean enable`<br/>`byte cuttingHeight` | Update Work Area settings |
| setStayOutZone | `String zoneId`<br/>`boolean enable` | Enable or disable stay-out zone |
| setCalendarTask | `Long workAreaId` (optional, set to `null` if the mower doesn't support Work Areas)<br/>`short[] start`<br/>`short[] duration`<br/>`boolean[] monday`<br/>`boolean[] tuesday`<br/>`boolean[] wednesday`<br/>`boolean[] thursday`<br/>`boolean[] friday`<br/>`boolean[] saturday`<br/>`boolean[] sunday` | Update calendar task settings. Parameter are an array for all calendar tasks (per Work Area) |
| poll | - | Poll mower status update from the cloud |
## Full Example
### automower.thing
```java
Bridge automower:bridge:mybridge [ appKey="<your_private_application_key>", userName="<your_username>", password="<your_password>" ] {
Thing automower myAutomower [ mowerId="<your_id_received_from_discovery>", pollingInterval=3600 ] {
Bridge automower:bridge:mybridge [ appKey="<your_private_application_key>", appSecret="<your_private_application_secret>", pollingInterval=300 ] {
Thing automower 12345678-1234-12ab-1234-123456abcdef [ mowerId="12345678-1234-12ab-1234-123456abcdef" ] { // 1234... is an example of the id recieved via discovery
}
}
```
@@ -209,24 +206,23 @@ Bridge automower:bridge:mybridge [ appKey="<your_private_application_key>", user
### automower.items
```java
String Automower_Mode "Mode [%s]" { channel="automower:automower:mybridge:myAutomower:status#mode" }
String Automower_Activity "Activity [%s]" { channel="automower:automower:mybridge:myAutomower:status#activity" }
String Automower_State "State [%s]" { channel="automower:automower:mybridge:myAutomower:status#state" }
DateTime Automower_Last_Update "Last Update" { channel="automower:automower:mybridge:myAutomower:status#last-update" }
Number Automower_Battery "Battery [%d %%]" { channel="automower:automower:mybridge:myAutomower:status#battery" }
Number Automower_Error_Code "Error Code [%d]" { channel="automower:automower:mybridge:myAutomower:status#error-code" }
DateTime Automower_Error_Time "Error Time" { channel="automower:automower:mybridge:myAutomower:status#error-timestamp" }
String Automower_Override_Action "Override Action [%s]" { channel="automower:automower:mybridge:myAutomower:status#override-action" }
DateTime Automower_Next_Start_Time "Next Start Time" { channel="automower:automower:mybridge:myAutomower:status#next-start" }
String Automower_Mode "Mode [%s]" { channel="automower:automower:mybridge:12345678-1234-12ab-1234-123456abcdef:status#mode" }
String Automower_Activity "Activity [%s]" { channel="automower:automower:mybridge:12345678-1234-12ab-1234-123456abcdef:status#activity" }
String Automower_State "State [%s]" { channel="automower:automower:mybridge:12345678-1234-12ab-1234-123456abcdef:status#state" }
DateTime Automower_Last_Update "Last Update" { channel="automower:automower:mybridge:12345678-1234-12ab-1234-123456abcdef:status#last-update" }
Number Automower_Battery "Battery [%d %%]" { channel="automower:automower:mybridge:12345678-1234-12ab-1234-123456abcdef:status#battery" }
Number Automower_Error_Code "Error Code [%d]" { channel="automower:automower:mybridge:12345678-1234-12ab-1234-123456abcdef:status#error-code" }
DateTime Automower_Error_Time "Error Time" { channel="automower:automower:mybridge:12345678-1234-12ab-1234-123456abcdef:status#error-timestamp" }
String Automower_Override_Action "Override Action [%s]" { channel="automower:automower:mybridge:12345678-1234-12ab-1234-123456abcdef:status#override-action" }
DateTime Automower_Next_Start_Time "Next Start Time" { channel="automower:automower:mybridge:12345678-1234-12ab-1234-123456abcdef:status#next-start" }
Location Automower_Position "Last Position" { channel="automower:automower:mybridge:12345678-1234-12ab-1234-123456abcdef:status#position" }
Number Automower_Command_Start "Start mowing for duration [%d min]" { channel="automower:automower:mybridge:myAutomower:command#start" }
Switch Automower_Command_Resume "Resume the schedule" { channel="automower:automower:mybridge:myAutomower:command#resume_schedule" }
Switch Automower_Command_Pause "Pause the automower" { channel="automower:automower:mybridge:myAutomower:command#pause" }
Number Automower_Command_Park "Park for duration [%d min]" { channel="automower:automower:mybridge:myAutomower:command#park" }
Switch Automower_Command_Park_Next_Schedule "Park until next schedule" { channel="automower:automower:mybridge:myAutomower:command#park_until_next_schedule" }
Switch Automower_Command_Park_Notice "Park until further notice" { channel="automower:automower:mybridge:myAutomower:command#park_until_further_notice" }
Location Automower_Last_Position "Last Position" { channel="automower:automower:mybridge:myAutomower:position#last-position" }
Number Automower_Command_Start "Start mowing for duration [%d min]" { channel="automower:automower:mybridge:12345678-1234-12ab-1234-123456abcdef:command#start" }
Switch Automower_Command_Resume "Resume the schedule" { channel="automower:automower:mybridge:12345678-1234-12ab-1234-123456abcdef:command#resume_schedule" }
Switch Automower_Command_Pause "Pause the automower" { channel="automower:automower:mybridge:12345678-1234-12ab-1234-123456abcdef:command#pause" }
Number Automower_Command_Park "Park for duration [%d min]" { channel="automower:automower:mybridge:12345678-1234-12ab-1234-123456abcdef:command#park" }
Switch Automower_Command_Park_Next_Schedule "Park until next schedule" { channel="automower:automower:mybridge:12345678-1234-12ab-1234-123456abcdef:command#park_until_next_schedule" }
Switch Automower_Command_Park_Notice "Park until further notice" { channel="automower:automower:mybridge:12345678-1234-12ab-1234-123456abcdef:command#park_until_further_notice" }
```
### automower.sitemap
@@ -244,7 +240,7 @@ sitemap demo label="Automower"
Text item=Automower_Error_Time
Text item=Automower_Override_Action
Text item=Automower_Next_Start_Time
Text item=Automower_Calendar_Tasks
Text item=Automower_Position
}
}
```
@@ -262,7 +258,7 @@ then
Automower_Command_Park_Notice.sendCommand(ON)
// alternative via actions
val mowerActions = getActions("automower", "automower:automower:mybridge:myAutomower")
val mowerActions = getActions("automower", "automower:automower:mybridge:12345678-1234-12ab-1234-123456abcdef")
mowerActions.parkUntilFurtherNotice()
end
```
@@ -270,4 +266,4 @@ end
## Footnotes
- <b id="f1">1)</b> ... Channel availability depends on Automower® capabilities [](#a1)
- <b id="f2">2)</b> ... Channel available for EPOS Automower® and systematic mowing work areas only [](#a2)
- <b id="f2">2)</b> ... Channel available for EPOS Automower® and systematic mowing Work Area only [](#a2)
@@ -59,6 +59,7 @@ public class AutomowerBindingConstants {
public static final String CHANNEL_STATUS_OVERRIDE_ACTION = GROUP_STATUS + "override-action";
public static final String CHANNEL_STATUS_RESTRICTED_REASON = GROUP_STATUS + "restricted-reason";
public static final String CHANNEL_STATUS_EXTERNAL_REASON = GROUP_STATUS + "external-reason";
public static final String CHANNEL_STATUS_POSITION = GROUP_STATUS + "position";
// List of all setting Channel ids
public static final String GROUP_SETTING = "setting#";
@@ -118,12 +119,6 @@ public class AutomowerBindingConstants {
new ChannelTypeUID(BINDING_ID, "calendarTaskWorkAreaIdType"),
new ChannelTypeUID(BINDING_ID, "workareaNameType")));
// Position Channel ids
public static final String GROUP_POSITION = "position#";
public static final String CHANNEL_POSITION_LAST = GROUP_POSITION + "last";
public static final String CHANNEL_POSITION = "pos";
// Stayout Zones Channel ids
public static final String GROUP_STAYOUTZONE = "stayoutzone#";
@@ -154,18 +149,17 @@ public class AutomowerBindingConstants {
// Messages Channel ids
public static final String GROUP_MESSAGE = "message#";
public static final ArrayList<String> CHANNEL_MESSAGE = new ArrayList<>(
List.of("msg-timestamp", "msg-code", "msg-text", "msg-severity", "msg-gps-position"));
public static final ArrayList<ChannelTypeUID> CHANNEL_TYPE_MESSAGE = new ArrayList<>(List.of(
new ChannelTypeUID(BINDING_ID, "messageTimeType"), new ChannelTypeUID(BINDING_ID, "messageCodeType"),
new ChannelTypeUID(BINDING_ID, "messageType"), new ChannelTypeUID(BINDING_ID, "messageSeverityType"),
new ChannelTypeUID(BINDING_ID, "messagePositionType")));
public static final String CHANNEL_MESSAGE_TIMESTAMP = GROUP_MESSAGE + "msg-timestamp";
public static final String CHANNEL_MESSAGE_CODE = GROUP_MESSAGE + "msg-code";
public static final String CHANNEL_MESSAGE_TEXT = GROUP_MESSAGE + "msg-text";
public static final String CHANNEL_MESSAGE_SEVERITY = GROUP_MESSAGE + "msg-severity";
public static final String CHANNEL_MESSAGE_GPS_POSITION = GROUP_MESSAGE + "msg-gps-position";
// Command Channel ids
public static final String GROUP_COMMAND = "command#";
public static final String CHANNEL_COMMAND_START = GROUP_COMMAND + "start";
public static final String CHANNEL_COMMAND_START_IN_WORK_AREA = GROUP_COMMAND + "start_in_work_area";
public static final String CHANNEL_COMMAND_RESUME_SCHEDULE = GROUP_COMMAND + "resume_schedule";
public static final String CHANNEL_COMMAND_PAUSE = GROUP_COMMAND + "pause";
public static final String CHANNEL_COMMAND_PARK = GROUP_COMMAND + "park";
@@ -189,15 +183,15 @@ public class AutomowerBindingConstants {
public static final ChannelTypeUID CHANNEL_TYPE_STATUS_WORK_AREA = new ChannelTypeUID(BINDING_ID, "workAreaType");
public static final ChannelTypeUID CHANNEL_TYPE_STATUS_ERROR_CONFIRMABLE = new ChannelTypeUID(BINDING_ID,
"errorConfirmableType");
public static final ChannelTypeUID CHANNEL_TYPE_STATUS_POSITION = new ChannelTypeUID(BINDING_ID, "positionType");
public static final ChannelTypeUID CHANNEL_TYPE_SETTING_HEADLIGHT_MODE = new ChannelTypeUID(BINDING_ID,
"settingHeadlightModeType");
public static final ChannelTypeUID CHANNEL_TYPE_POSITION_LAST = new ChannelTypeUID(BINDING_ID, "lastPositionType");
public static final ChannelTypeUID CHANNEL_TYPE_POSITION = new ChannelTypeUID(BINDING_ID, "positionType");
public static final ChannelTypeUID CHANNEL_TYPE_STAYOUTZONES_DIRTY = new ChannelTypeUID(BINDING_ID,
"zoneDirtyType");
public static final Map<Integer, String> ERROR = new HashMap<>() {
private static final long serialVersionUID = 1L;
{
put(0, "No message");
put(1, "Outside working area");
@@ -278,7 +272,7 @@ public class AutomowerBindingConstants {
put(76, "Connection NOT changed");
put(77, "Com board not available");
put(78, "Slipped - Mower has Slipped. Situation not solved with moving pattern");
put(79, "Invalid battery combination - Invalid combination of different battery types.");
put(79, "Invalid battery combination - Invalid combination of different battery types");
put(80, "Cutting system imbalance Warning");
put(81, "Safety function faulty");
put(82, "Wheel motor blocked, rear right");
@@ -29,6 +29,7 @@ import org.openhab.core.auth.client.oauth2.OAuthFactory;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.io.net.http.WebSocketFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
@@ -57,13 +58,15 @@ public class AutomowerHandlerFactory extends BaseThingHandlerFactory {
protected final @NonNullByDefault({}) HttpClient httpClient;
private @Nullable ServiceRegistration<?> automowerDiscoveryServiceRegistration;
private final TimeZoneProvider timeZoneProvider;
private final WebSocketFactory webSocketFactory;
@Activate
public AutomowerHandlerFactory(@Reference OAuthFactory oAuthFactory, @Reference HttpClientFactory httpClientFactory,
@Reference TimeZoneProvider timeZoneProvider) {
@Reference TimeZoneProvider timeZoneProvider, @Reference WebSocketFactory webSocketFactory) {
this.oAuthFactory = oAuthFactory;
this.httpClient = httpClientFactory.getCommonHttpClient();
this.timeZoneProvider = timeZoneProvider;
this.webSocketFactory = webSocketFactory;
}
@Override
@@ -74,7 +77,8 @@ public class AutomowerHandlerFactory extends BaseThingHandlerFactory {
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
if (AutomowerBridgeHandler.SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID())) {
AutomowerBridgeHandler handler = new AutomowerBridgeHandler((Bridge) thing, oAuthFactory, httpClient);
AutomowerBridgeHandler handler = new AutomowerBridgeHandler((Bridge) thing, oAuthFactory, httpClient,
webSocketFactory.getCommonWebSocketClient());
registerAutomowerDiscoveryService(handler);
return handler;
}
@@ -89,8 +93,8 @@ public class AutomowerHandlerFactory extends BaseThingHandlerFactory {
@Override
protected synchronized void removeHandler(ThingHandler thingHandler) {
if (thingHandler instanceof AutomowerBridgeHandler) {
ServiceRegistration<?> automowerDiscoveryServiceRegistration = this.automowerDiscoveryServiceRegistration;
if (automowerDiscoveryServiceRegistration != null) {
// remove discovery service, if bridge handler is removed
automowerDiscoveryServiceRegistration.unregister();
}
}
@@ -49,7 +49,7 @@ public class AutomowerActions implements ThingActions {
@RuleAction(label = "@text/action-start-label", description = "@text/action-start-desc")
public void start(
@ActionInput(name = "duration", label = "@text/action-input-duration-label", description = "@text/action-input-duration-desc") int durationMin) {
@ActionInput(name = "duration-min", label = "@text/action-input-duration-min-label", description = "@text/action-input-duration-min-desc") long durationMin) {
AutomowerHandler automowerHandler = handler;
if (automowerHandler == null) {
logger.warn("Automower Action service ThingHandler is null!");
@@ -58,10 +58,26 @@ public class AutomowerActions implements ThingActions {
}
}
public static void start(ThingActions actions, int durationMin) {
public static void start(ThingActions actions, long durationMin) {
((AutomowerActions) actions).start(durationMin);
}
@RuleAction(label = "@text/action-start-in-work-area-label", description = "@text/action-start-in-work-area-desc")
public void startInWorkArea(
@ActionInput(name = "work-area-id", label = "@text/action-input-work-area-id-label", description = "@text/action-input-work-area-id-desc") long workAreaId,
@ActionInput(name = "duration-min-opt", label = "@text/action-input-duration-min-opt-label", description = "@text/action-input-duration-min-opt-desc") @Nullable Long durationMin) {
AutomowerHandler automowerHandler = handler;
if (automowerHandler == null) {
logger.warn("Automower Action service ThingHandler is null!");
} else {
automowerHandler.sendAutomowerCommand(AutomowerCommand.START_IN_WORK_AREA, workAreaId, durationMin);
}
}
public static void startInWorkArea(ThingActions actions, long workAreaId, @Nullable Long durationMin) {
((AutomowerActions) actions).startInWorkArea(workAreaId, durationMin);
}
@RuleAction(label = "@text/action-pause-label", description = "@text/action-pause-desc")
public void pause() {
AutomowerHandler automowerHandler = handler;
@@ -106,7 +122,7 @@ public class AutomowerActions implements ThingActions {
@RuleAction(label = "@text/action-park-label", description = "@text/action-park-desc")
public void park(
@ActionInput(name = "duration", label = "@text/action-input-duration-label", description = "@text/action-input-duration-desc") int durationMin) {
@ActionInput(name = "duration-min", label = "@text/action-input-duration-min-label", description = "@text/action-input-duration-min-desc") long durationMin) {
AutomowerHandler automowerHandler = handler;
if (automowerHandler == null) {
logger.warn("Automower Action service ThingHandler is null!");
@@ -115,7 +131,7 @@ public class AutomowerActions implements ThingActions {
}
}
public static void park(ThingActions actions, int durationMin) {
public static void park(ThingActions actions, long durationMin) {
((AutomowerActions) actions).park(durationMin);
}
@@ -187,7 +203,7 @@ public class AutomowerActions implements ThingActions {
@RuleAction(label = "@text/action-set-work-area-label", description = "@text/action-set-work-area-desc")
public void setWorkArea(
@ActionInput(name = "workarea-id", label = "@text/action-input-workarea-id-label", description = "@text/action-input-workarea-id-desc") long workAreaId,
@ActionInput(name = "work-area-id", label = "@text/action-input-work-area-id-label", description = "@text/action-input-work-area-id-desc") long workAreaId,
@ActionInput(name = "enable", label = "@text/action-input-enable-label", description = "@text/action-input-enable-desc") boolean enable,
@ActionInput(name = "cutting-height", label = "@text/action-input-cutting-height-label", description = "@text/action-input-cutting-height-desc") byte cuttingHeight) {
AutomowerHandler automowerHandler = handler;
@@ -220,7 +236,7 @@ public class AutomowerActions implements ThingActions {
@RuleAction(label = "@text/action-set-calendartask-label", description = "@text/action-set-calendartask-desc")
public void setCalendarTask(
@ActionInput(name = "workarea-id", label = "@text/action-input-workarea-id-label", description = "@text/action-input-workarea-id-desc") @Nullable Long workAreaId,
@ActionInput(name = "work-area-id", label = "@text/action-input-work-area-id-label", description = "@text/action-input-work-area-id-desc") @Nullable Long workAreaId,
@ActionInput(name = "start", label = "@text/action-input-start-label", description = "@text/action-input-start-desc") short[] start,
@ActionInput(name = "duration", label = "@text/action-input-duration-label", description = "@text/action-input-duration-desc") short[] duration,
@ActionInput(name = "monday", label = "@text/action-input-monday-label", description = "@text/action-input-monday-desc") boolean[] monday,
@@ -68,7 +68,7 @@ public class AutomowerBridge {
this.automowerApi = new AutomowerConnectApi(httpClient);
}
private AccessTokenResponse authenticate() throws AutomowerCommunicationException {
public synchronized AccessTokenResponse authenticate() throws AutomowerCommunicationException {
try {
AccessTokenResponse result = authService.getAccessTokenResponse();
if (result == null || result.isExpired(Instant.now(), 120)) {
@@ -112,17 +112,27 @@ public class AutomowerBridge {
* @param id The id of the mower
* @param command The command that should be sent. Valid values are: "Start", "ResumeSchedule", "Pause", "Park",
* "ParkUntilNextSchedule", "ParkUntilFurtherNotice"
* @param commandDuration The duration of the command. This is only evaluated for "Start" and "Park" commands
* @param commandDuration The duration of the command. This is only evaluated for "Start", "StartInWorkArea" and
* "Park" commands
* @param commandWorkAreaId The work area id to be used for the command. This is only evaluated for
* "StartInWorkArea" command
* @throws AutomowerCommunicationException In case the query cannot be executed successfully
*/
public void sendAutomowerCommand(String id, AutomowerCommand command, long commandDuration)
throws AutomowerCommunicationException {
MowerCommandAttributes attributes = new MowerCommandAttributes();
attributes.setDuration(commandDuration);
public void sendAutomowerCommand(String id, AutomowerCommand command, @Nullable Long commandWorkAreaId,
@Nullable Long commandDuration) throws AutomowerCommunicationException {
MowerCommand mowerCommand = new MowerCommand();
mowerCommand.setType(command.getCommand());
mowerCommand.setAttributes(attributes);
if (commandDuration != null || commandWorkAreaId != null) {
MowerCommandAttributes attributes = new MowerCommandAttributes();
if (commandDuration != null) {
attributes.setDuration(commandDuration);
}
if (commandWorkAreaId != null) {
attributes.setWorkAreaId(commandWorkAreaId);
}
mowerCommand.setAttributes(attributes);
}
MowerCommandRequest request = new MowerCommandRequest();
request.setData(mowerCommand);
@@ -14,16 +14,26 @@ package org.openhab.binding.automower.internal.bridge;
import static org.openhab.binding.automower.internal.AutomowerBindingConstants.THING_TYPE_BRIDGE;
import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.eclipse.jetty.websocket.common.WebSocketSession;
import org.openhab.binding.automower.internal.rest.api.automowerconnect.dto.Mower;
import org.openhab.binding.automower.internal.rest.api.automowerconnect.dto.MowerListResult;
import org.openhab.binding.automower.internal.rest.exceptions.AutomowerCommunicationException;
import org.openhab.binding.automower.internal.things.AutomowerHandler;
import org.openhab.core.auth.client.oauth2.OAuthClientService;
import org.openhab.core.auth.client.oauth2.OAuthFactory;
import org.openhab.core.thing.Bridge;
@@ -52,24 +62,86 @@ public class AutomowerBridgeHandler extends BaseBridgeHandler {
private static final long DEFAULT_POLLING_INTERVAL_S = TimeUnit.HOURS.toSeconds(1);
private final OAuthFactory oAuthFactory;
private @Nullable WebSocketSession webSocketSession;
private @Nullable OAuthClientService oAuthService;
private @Nullable ScheduledFuture<?> automowerBridgePollingJob;
private @Nullable AutomowerBridge bridge;
private final HttpClient httpClient;
private final WebSocketClient webSocketClient;
private boolean closing;
private Map<String, AutomowerHandler> automowerHandlers = new HashMap<>();
public AutomowerBridgeHandler(Bridge bridge, OAuthFactory oAuthFactory, HttpClient httpClient) {
public AutomowerBridgeHandler(Bridge bridge, OAuthFactory oAuthFactory, HttpClient httpClient,
WebSocketClient webSocketClient) {
super(bridge);
this.oAuthFactory = oAuthFactory;
this.httpClient = httpClient;
this.webSocketClient = webSocketClient;
}
private void pollAutomowers(AutomowerBridge bridge) {
public void registerAutomowerHandler(String mowerId, AutomowerHandler handler) {
automowerHandlers.put(mowerId, handler);
logger.trace("Registered AutomowerHandler for mower with ID: {}", mowerId);
}
public void unregisterAutomowerHandler(String mowerId) {
automowerHandlers.remove(mowerId);
logger.trace("Unregistered AutomowerHandler for mower with ID: {}", mowerId);
}
public WebSocketClient getWebSocketClient() {
return webSocketClient;
}
public ScheduledExecutorService getScheduler() {
return scheduler;
}
public @Nullable WebSocketSession getWebSocketSession() {
return webSocketSession;
}
public void setWebSocketSession(@Nullable WebSocketSession webSocketSession) {
this.webSocketSession = webSocketSession;
}
public boolean isClosing() {
return closing;
}
public void setClosing(boolean closing) {
this.closing = closing;
}
public @Nullable AutomowerHandler getAutomowerHandlerByThingId(@Nullable String thingId) {
return automowerHandlers.get(thingId);
}
public synchronized void pollAutomowers(AutomowerBridge bridge) {
MowerListResult automowers;
try {
automowers = bridge.getAutomowers();
updateStatus(ThingStatus.ONLINE);
logger.debug("Found {} automowers", automowers.getData().size());
List<Mower> mowers = automowers.getData();
if (mowers == null || mowers.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/comm-error-no-mowers-found");
logger.debug("No automowers found in the response from REST API");
} else {
updateStatus(ThingStatus.ONLINE);
logger.debug("Found {} automowers in the response from REST API", mowers.size());
// Update all known AutomowerHandlers with the data from the REST API
for (Mower mower : mowers) {
String id = mower.getId();
AutomowerHandler automowerHandler = getAutomowerHandlerByThingId(id);
if (automowerHandler != null) {
logger.debug("Data from REST API for known AutomowerHandler with id: {}", id);
automowerHandler.updateAutomowerStateViaREST(mower);
} else {
logger.debug("Data from REST API for unknown AutomowerHandler with id: {}", id);
}
}
}
} catch (AutomowerCommunicationException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/comm-error-query-mowers-failed");
@@ -79,16 +151,29 @@ public class AutomowerBridgeHandler extends BaseBridgeHandler {
@Override
public void dispose() {
closing = true;
AutomowerBridge currentBridge = bridge;
if (currentBridge != null) {
stopAutomowerBridgePolling(currentBridge);
bridge = null;
}
WebSocketSession webSocketSession = this.webSocketSession;
if (webSocketSession != null) {
try {
webSocketSession.close();
} catch (Exception e) {
logger.error("Failed to close WebSocket session: {}", e.getMessage());
}
}
OAuthClientService oAuthService = this.oAuthService;
if (oAuthService != null) {
oAuthFactory.ungetOAuthService(thing.getUID().getAsString());
this.oAuthService = null;
}
logger.debug("Bridge {} disposed", thing.getUID().getAsString());
}
@Override
@@ -111,9 +196,12 @@ public class AutomowerBridgeHandler extends BaseBridgeHandler {
HUSQVARNA_API_TOKEN_URL, null, appKey, appSecret, null, null);
this.oAuthService = oAuthService;
if (bridge == null) {
if (this.bridge == null) {
AutomowerBridge currentBridge = new AutomowerBridge(oAuthService, appKey, httpClient, scheduler);
bridge = currentBridge;
this.bridge = currentBridge;
// connect WebSocket and poll automower state via REST API once after connection
connectWebSocket(new AutomowerWebSocketAdapter(this, currentBridge));
// setup polling of automowers via REST API
startAutomowerBridgePolling(currentBridge, pollingIntervalS);
}
updateStatus(ThingStatus.UNKNOWN);
@@ -130,8 +218,8 @@ public class AutomowerBridgeHandler extends BaseBridgeHandler {
ScheduledFuture<?> currentPollingJob = automowerBridgePollingJob;
if (currentPollingJob == null) {
final long pollingIntervalToUse = pollingIntervalS == null ? DEFAULT_POLLING_INTERVAL_S : pollingIntervalS;
automowerBridgePollingJob = scheduler.scheduleWithFixedDelay(() -> pollAutomowers(bridge), 1,
pollingIntervalToUse, TimeUnit.SECONDS);
automowerBridgePollingJob = scheduler.scheduleWithFixedDelay(() -> pollAutomowers(bridge),
pollingIntervalToUse, pollingIntervalToUse, TimeUnit.SECONDS);
}
}
@@ -164,4 +252,26 @@ public class AutomowerBridgeHandler extends BaseBridgeHandler {
return Optional.empty();
}
}
public void connectWebSocket(AutomowerWebSocketAdapter webSocketAdapter) {
try {
AutomowerBridge bridge = this.bridge;
if (bridge != null) {
String accessToken = bridge.authenticate().getAccessToken();
if (accessToken == null) {
logger.error("No OAuth2 access token available for WebSocket connection");
return;
}
String wsUrl = "wss://ws.openapi.husqvarna.dev/v1";
ClientUpgradeRequest request = new ClientUpgradeRequest();
request.setHeader("Authorization", "Bearer " + accessToken);
webSocketSession = (WebSocketSession) webSocketClient
.connect(webSocketAdapter, URI.create(wsUrl), request).get();
} else {
logger.error("Bridge is null, cannot connect WebSocket");
}
} catch (Exception e) {
logger.error("Failed to start WebSocket client: {}", e.getMessage());
}
}
}
@@ -0,0 +1,187 @@
/*
* 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.automower.internal.bridge;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketFrame;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.api.extensions.Frame;
import org.eclipse.jetty.websocket.common.WebSocketSession;
import org.eclipse.jetty.websocket.common.frames.PongFrame;
import org.openhab.binding.automower.internal.rest.exceptions.AutomowerCommunicationException;
import org.openhab.binding.automower.internal.things.AutomowerHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
/**
* The {@link AutomowerWebSocketAdapter} handles the WebSocket Connection of the Husqvarna web service.
*
* @author MikeTheTux - Initial contribution
*/
@NonNullByDefault
@WebSocket
public class AutomowerWebSocketAdapter {
private final AutomowerBridgeHandler handler;
private final AutomowerBridge bridge;
private final Logger logger = LoggerFactory.getLogger(AutomowerWebSocketAdapter.class);
private @Nullable ScheduledFuture<?> connectionTracker;
private int unansweredPings = 0;
private static final int MAX_UNANSWERED_PINGS = 5;
private ByteBuffer pingPayload = ByteBuffer.wrap("ping".getBytes(StandardCharsets.UTF_8));
public AutomowerWebSocketAdapter(AutomowerBridgeHandler handler, AutomowerBridge bridge) {
this.handler = handler;
this.bridge = bridge;
}
@OnWebSocketConnect
public synchronized void onConnect(Session session) {
handler.setClosing(false);
unansweredPings = 0;
logger.debug("Connected to Husqvarna WebSocket ({})", session.getRemoteAddress().getHostString());
// Initialize MowerStatus via polling the REST API
logger.debug("Polling Automowers for initial state / after reconnect of WebSocket");
// Poll all automowers to get their initial state
// This is necessary because the WebSocket does not provide the initial state
// and we need to have the initial state before subscribing to the WebSocket messages
// This will also recover potential lost WebSocket messages during WebSocket reconnect
handler.pollAutomowers(bridge);
// Subscribe to all messages after connecting
try {
String subscribeAllMessage = "{\"type\":\"subscribe\",\"topics\":[\"*\"]}";
session.getRemote().sendString(subscribeAllMessage);
logger.debug("Sent subscription message to subscribe to all topics");
} catch (Exception e) {
logger.error("Failed to send subscription message: {}", e.getMessage());
}
// Cancel previous ping task if it exists
ScheduledFuture<?> connectionTracker = this.connectionTracker;
if (connectionTracker != null && !connectionTracker.isCancelled()) {
logger.trace("Cancelling previous connectionTracker (ping)");
connectionTracker.cancel(true);
}
logger.trace("Starting connectionTracker (ping)");
// start sending PING every minute
this.connectionTracker = handler.getScheduler().scheduleWithFixedDelay(this::sendKeepAlivePing, 1, 1,
TimeUnit.MINUTES);
}
@OnWebSocketFrame
public synchronized void onFrame(Frame pong) {
if (pong instanceof PongFrame) {
unansweredPings = 0;
// logger.trace("Pong received");
}
}
@OnWebSocketMessage
public void onMessage(String message) {
try {
if (!message.isBlank()) {
JsonObject event = JsonParser.parseString(message).getAsJsonObject();
if (event.has("id")) {
String id = event.get("id").getAsString();
AutomowerHandler automowerHandler = handler.getAutomowerHandlerByThingId(id);
if (automowerHandler != null) {
logger.debug("Message from WebSocket for known AutomowerHandler: {}", message);
automowerHandler.processWebSocketMessage(event);
} else {
logger.debug("Message from WebSocket for unknown AutomowerHandler: {}", message);
}
} else {
logger.trace("Message from WebSocket without Id: {}", message);
}
} else {
logger.trace("Received empty message from WebSocket");
}
} catch (Exception e) {
logger.error("Failed to process WebSocket message: {}", e.getMessage());
}
}
@OnWebSocketClose
public void onClose(int statusCode, String reason) {
logger.debug("WebSocket closed: {} - {}", statusCode, reason);
// Cancel ping task on disconnect
final ScheduledFuture<?> connectionTracker = this.connectionTracker;
if (connectionTracker != null) {
logger.trace("Cancelling connectionTracker (ping)");
connectionTracker.cancel(true);
}
if (!handler.isClosing()) {
try {
logger.debug("Reconnecting to Husqvarna Webservice ()");
handler.connectWebSocket(this);
} catch (Exception e) {
logger.error("Failed to restart WebSocket client: {}", e.getMessage());
}
}
}
@OnWebSocketError
public void onError(Throwable cause) {
logger.error("WebSocket error: {}", cause.getMessage());
}
/**
* Sends a ping to tell the Husqvarna smart system that the client is alive.
*/
private synchronized void sendKeepAlivePing() {
try {
WebSocketSession webSocketSession = handler.getWebSocketSession();
if (webSocketSession != null) {
String accessToken = bridge.authenticate().getAccessToken();
if (unansweredPings > MAX_UNANSWERED_PINGS || accessToken == null) {
webSocketSession.close(1000, "Timeout: manually closing dead connection");
} else {
if (webSocketSession.isOpen()) {
try {
// logger.trace("Sending ping ...");
webSocketSession.getRemote().sendPing(pingPayload);
++unansweredPings;
} catch (IOException ex) {
logger.warn("Error while sending ping: {}", ex.getMessage());
}
}
}
} else {
logger.warn("WebSocket session is null, cannot send ping");
}
} catch (AutomowerCommunicationException e) {
logger.error("Failed to authenticate while sending keep-alive ping: {}", e.getMessage());
}
}
}
@@ -64,7 +64,7 @@ public class AutomowerConnectApi extends HusqvarnaApi {
request.method(HttpMethod.GET);
ContentResponse response = executeRequest(appKey, token, request);
logger.trace("getMowers: {}", response.getContentAsString());
return parseResponse(response, MowerListResult.class);
}
@@ -16,13 +16,22 @@ package org.openhab.binding.automower.internal.rest.api.automowerconnect.dto;
* @author Markus Pfleger - Initial contribution
*/
public class MowerCommandAttributes {
private long duration;
private Long duration;
private Long workAreaId;
public long getDuration() {
public Long getDuration() {
return duration;
}
public void setDuration(long duration) {
public void setDuration(Long duration) {
this.duration = duration;
}
public Long getWorkAreaId() {
return workAreaId;
}
public void setWorkAreaId(Long workAreaId) {
this.workAreaId = workAreaId;
}
}
@@ -40,7 +40,7 @@ public class StayOutZone {
return enabled;
}
public void setEnabed(Boolean enabled) {
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
}
@@ -27,6 +27,7 @@ import org.openhab.core.thing.ChannelUID;
@NonNullByDefault
public enum AutomowerCommand {
START("Start", AutomowerBindingConstants.CHANNEL_COMMAND_START),
START_IN_WORK_AREA("StartInWorkArea", AutomowerBindingConstants.CHANNEL_COMMAND_START_IN_WORK_AREA),
RESUME_SCHEDULE("ResumeSchedule", AutomowerBindingConstants.CHANNEL_COMMAND_RESUME_SCHEDULE),
PAUSE("Pause", AutomowerBindingConstants.CHANNEL_COMMAND_PAUSE),
PARK("Park", AutomowerBindingConstants.CHANNEL_COMMAND_PARK),
@@ -23,7 +23,6 @@ import org.eclipse.jdt.annotation.Nullable;
@NonNullByDefault
public class AutomowerConfiguration {
public @Nullable String mowerId;
public @Nullable Integer pollingInterval;
public @Nullable String mowerZoneId;
@Nullable
@@ -35,14 +34,6 @@ public class AutomowerConfiguration {
this.mowerId = mowerId;
}
public @Nullable Integer getPollingInterval() {
return pollingInterval;
}
public void setPollingInterval(Integer pollingInterval) {
this.pollingInterval = pollingInterval;
}
@Nullable
public String getMowerZoneId() {
return mowerZoneId;
@@ -5,7 +5,7 @@
<type>binding</type>
<name>Automower Binding</name>
<description>Binding to interact with Husquvarna Automower robots.</description>
<description>Binding for Husqvarna Automower® robotic lawn mowers</description>
<connection>cloud</connection>
</addon:addon>
@@ -1,36 +1,33 @@
# add-on
addon.automower.name = Automower Binding
addon.automower.description = Binding to interact with Husquvarna Automower robots.
addon.automower.description = Binding for Husqvarna Automower® robotic lawn mowers
# thing types
thing-type.automower.automower.label = Automower
thing-type.automower.automower.label = Automower®
thing-type.automower.automower.description = An automatic lawn mower
thing-type.automower.bridge.label = Automower Connect Bridge
thing-type.automower.bridge.description = The bridge to communicate with the Automower Connect API
thing-type.automower.bridge.label = Automower® Connect Bridge
thing-type.automower.bridge.description = The bridge to communicate with the Automower® Connect API
# thing types config
thing-type.config.automower.automower.mowerId.label = Automower Id
thing-type.config.automower.automower.mowerId.description = The Id of an automower as used by the Automower Connect Api to identify a mower
thing-type.config.automower.automower.mowerId.label = Automower® Id
thing-type.config.automower.automower.mowerId.description = The Id of an Automower® as used by the Automower® Connect API to identify a Automower®. This is automatically filled when the thing is discovered.
thing-type.config.automower.automower.mowerZoneId.label = TimeZone
thing-type.config.automower.automower.mowerZoneId.description = Time zone of the mower (e.g. Europe/Berlin)
thing-type.config.automower.automower.pollingInterval.label = Polling Interval
thing-type.config.automower.automower.pollingInterval.description = How often the current automower state should be polled in seconds
thing-type.config.automower.automower.mowerZoneId.description = Time zone of the Automower® (e.g. Europe/Berlin). Default is the time zone configured in openHAB
thing-type.config.automower.bridge.appKey.label = Application Key
thing-type.config.automower.bridge.appKey.description = The Application Key is required to communicate with the Automower Connect API at https://developer.husqvarnagroup.cloud/. It can be obtained by registering an Application on the Husqvarna Website. This application also needs to be connected to the "Authentication API" and the "Automower Connect API"
thing-type.config.automower.bridge.appKey.description = The Application Key is required to communicate with the Automower® Connect API. It can be obtained by registering an Application on https://developer.husqvarnagroup.cloud/. This application needs to be connected to the "Authentication API" and the "Automower® Connect API". https://developer.husqvarnagroup.cloud/docs/getting-started
thing-type.config.automower.bridge.appSecret.label = Application Secret
thing-type.config.automower.bridge.appSecret.description = The Application Secret is required to communicate with the Automower Connect API at https://developer.husqvarnagroup.cloud/. It can be obtained by registering an Application on the Husqvarna Website.
thing-type.config.automower.bridge.appSecret.description = The Application Secret is required to communicate with the Automower® Connect API. See "Application Key" for further details
thing-type.config.automower.bridge.pollingInterval.label = Polling Interval
thing-type.config.automower.bridge.pollingInterval.description = How often the available automowers should be queried in seconds
thing-type.config.automower.bridge.pollingInterval.description = How often the current Automower® states should be polled using REST API
# channel group types
channel-group-type.automower.calendarTasksType.label = Calendar Tasks
channel-group-type.automower.commandsType.label = Commands
channel-group-type.automower.messagesType.label = Messages
channel-group-type.automower.positionsType.label = Positions
channel-group-type.automower.settingsType.label = Settings
channel-group-type.automower.statisticsType.label = Statistics
channel-group-type.automower.statusType.label = Status
@@ -50,7 +47,7 @@ channel-type.automower.activityType.state.option.LEAVING = Leaving charging stat
channel-type.automower.activityType.state.option.PARKED_IN_CS = Parked in charging station
channel-type.automower.activityType.state.option.STOPPED_IN_GARDEN = Stopped in garden
channel-type.automower.batteryType.label = Battery
channel-type.automower.batteryType.description = The battery level of the mower at the time of last update
channel-type.automower.batteryType.description = The battery state of charge in percent
channel-type.automower.calendarTaskDurationType.label = Duration
channel-type.automower.calendarTaskDurationType.description = Duration time
channel-type.automower.calendarTaskDurationType.state.pattern = %1$tHh:%1$tMm
@@ -74,36 +71,34 @@ channel-type.automower.calendarTaskWednesdayType.description = Enabled on Wednes
channel-type.automower.calendarTaskWorkAreaIdType.label = Work Area Id of Calendar
channel-type.automower.calendarTaskWorkAreaIdType.description = Work Area Id mapped to this calendar
channel-type.automower.errorCodeType.label = Error Code
channel-type.automower.errorCodeType.description = The error code at the time of last update
channel-type.automower.errorCodeType.description = The current error code
channel-type.automower.errorConfirmableType.label = Error Confirmable
channel-type.automower.errorConfirmableType.description = If the mower has an Error Code this attribute states if the error is confirmable
channel-type.automower.errorConfirmableType.description = If the Automower® has an error, this attribute states if the error is confirmable
channel-type.automower.errorMessageType.label = Error Message
channel-type.automower.errorMessageType.description = The error message at the time of last update
channel-type.automower.errorMessageType.description = The current error message
channel-type.automower.errorTimestampType.label = Error Time
channel-type.automower.errorTimestampType.description = The time when the error occurred
channel-type.automower.errorTimestampType.description = The time when the current error occurred
channel-type.automower.externalReasonType.label = External Reason
channel-type.automower.externalReasonType.description = The channel providing an external reason that restrics current planner operation.
channel-type.automower.externalReasonType.description = An external reason set by i.e. Google Assistant or Amazon Alexa that restrics the current planner operation
channel-type.automower.inactiveReasonType.label = Inactive Reason
channel-type.automower.inactiveReasonType.description = The current reason for being inactive
channel-type.automower.inactiveReasonType.state.option.NONE = No inactive reason
channel-type.automower.inactiveReasonType.state.option.PLANNING = The mower is planning a path or a work area
channel-type.automower.inactiveReasonType.state.option.PLANNING = The Automower® is planning a path or a Work Area
channel-type.automower.inactiveReasonType.state.option.SEARCHING_FOR_SATELLITES = Waiting for fix when using EPOS
channel-type.automower.lastPollUpdateType.label = Last Poll Update
channel-type.automower.lastPollUpdateType.description = The time when the binding polled the last update from the cloud
channel-type.automower.lastPositionType.label = Last GPS Position
channel-type.automower.lastPositionType.description = Last GPS position of the mower
channel-type.automower.lastUpdateType.label = Last Update
channel-type.automower.lastUpdateType.description = The time when the mower sent the last update
channel-type.automower.messageCodeType.label = Code
channel-type.automower.messageCodeType.description = (Error) code of the event
channel-type.automower.messagePositionType.label = GPS Position
channel-type.automower.messagePositionType.description = GPS Position of the event (if available)
channel-type.automower.messageSeverityType.label = Severity
channel-type.automower.messageSeverityType.description = The severity of the event
channel-type.automower.messageTimeType.label = Timestamp
channel-type.automower.messageTimeType.description = Timestamp when the event occurred
channel-type.automower.messageType.label = Message
channel-type.automower.messageType.description = The Message
channel-type.automower.lastUpdateType.description = The time when the Automower® sent the last update
channel-type.automower.messageCodeType.label = Last Error Code
channel-type.automower.messageCodeType.description = The last error code
channel-type.automower.messagePositionType.label = Message GPS Position
channel-type.automower.messagePositionType.description = GPS position of the last event (if available)
channel-type.automower.messageSeverityType.label = Message Severity
channel-type.automower.messageSeverityType.description = The severity of the last error
channel-type.automower.messageTimeType.label = Last Error Time
channel-type.automower.messageTimeType.description = The time when the last error occurred
channel-type.automower.messageType.label = Last Error Message
channel-type.automower.messageType.description = The last error message
channel-type.automower.modeType.label = Mode
channel-type.automower.modeType.description = The current mode
channel-type.automower.modeType.state.option.MAIN_AREA = Main Area
@@ -111,26 +106,26 @@ channel-type.automower.modeType.state.option.SECONDARY_AREA = Secondary Area
channel-type.automower.modeType.state.option.HOME = Home
channel-type.automower.modeType.state.option.DEMO = Demo
channel-type.automower.modeType.state.option.UNKNOWN = Unknown
channel-type.automower.nameType.label = Automower Name
channel-type.automower.nameType.description = The channel providing the automower name
channel-type.automower.nextStartType.label = Next Auto Start
channel-type.automower.nextStartType.description = The channel providing the time for the next auto start. If the mower is charging then the value is the estimated time when it will be leaving the charging station. If the mower is about to start now, the value is NULL.
channel-type.automower.nameType.label = Automower® Name
channel-type.automower.nameType.description = The name of the Automower®
channel-type.automower.nextStartType.label = Next Start
channel-type.automower.nextStartType.description = The time for the next scheduled start. If the Automower® is charging then the value is the estimated time when it will be leaving the charging station. If the Automower® is about to start now, the value is NULL
channel-type.automower.overrideActionType.label = Override Action
channel-type.automower.overrideActionType.description = The channel providing an action that overrides current planner operation.
channel-type.automower.overrideActionType.description = The action that overrides the current planner operation
channel-type.automower.park.label = Park with Duration
channel-type.automower.park.description = Park for a duration in minutes
channel-type.automower.park.description = Park the Automower® for the given duration, overriding the schedule
channel-type.automower.parkUntilFurtherNotice.label = Park and Pause the Schedule
channel-type.automower.parkUntilFurtherNotice.description = Park and pause the mower schedule until manual resume
channel-type.automower.parkUntilFurtherNotice.description = Park the Automower® until it is started again by the start action/command or the schedule gets resumed
channel-type.automower.parkUntilNextSchedule.label = Park Until Next Schedule
channel-type.automower.parkUntilNextSchedule.description = Park until next schedule
channel-type.automower.parkUntilNextSchedule.description = Park the Automower®, fully charge it and start afterwards according to the schedule
channel-type.automower.pause.label = Pause
channel-type.automower.pause.description = Pause the mower now until manual resume
channel-type.automower.pause.description = Pause the Automower® at the current location until manual resume
channel-type.automower.pollUpdateType.label = Poll Update
channel-type.automower.pollUpdateType.description = Poll mower status update from the cloud
channel-type.automower.positionType.label = GPS Position
channel-type.automower.positionType.description = A waypoint of the mower's activity
channel-type.automower.pollUpdateType.description = Poll Automower® status update from the cloud
channel-type.automower.positionType.label = Last GPS Position
channel-type.automower.positionType.description = Last GPS position of the Automower®
channel-type.automower.restrictedReasonType.label = Restricted Reason
channel-type.automower.restrictedReasonType.description = The channel providing a reason that restrics current planner operation.
channel-type.automower.restrictedReasonType.description = The reason that restrics the current planner operation
channel-type.automower.restrictedReasonType.state.option.NONE = No restricted reason
channel-type.automower.restrictedReasonType.state.option.WEEK_SCHEDULE = Restricted by week schedule
channel-type.automower.restrictedReasonType.state.option.PARK_OVERRIDE = Forced to park
@@ -138,25 +133,26 @@ channel-type.automower.restrictedReasonType.state.option.SENSOR = Restricted by
channel-type.automower.restrictedReasonType.state.option.DAILY_LIMIT = Restricted by daily limit
channel-type.automower.restrictedReasonType.state.option.FOTA = Restricted by FOTA transfer
channel-type.automower.restrictedReasonType.state.option.FROST = Restricted by frost sensor
channel-type.automower.restrictedReasonType.state.option.ALL_WORK_AREAS_COMPLETED = Restricted: All work areas completed
channel-type.automower.restrictedReasonType.state.option.ALL_WORK_AREAS_COMPLETED = Restricted: All Work Areas completed
channel-type.automower.restrictedReasonType.state.option.EXTERNAL = Restricted by external reason
channel-type.automower.resumeSchedule.label = Resume Schedule
channel-type.automower.resumeSchedule.description = Resume schedule
channel-type.automower.resumeSchedule.description = Resume the schedule of the Automower®
channel-type.automower.settingCuttingHeightType.label = Cutting Height
channel-type.automower.settingCuttingHeightType.description = Prescaled cutting height, range: 1-9
channel-type.automower.settingHeadlightModeType.label = Headlight Mode
channel-type.automower.settingHeadlightModeType.description = Information about headlights
channel-type.automower.start.label = Start with Duration
channel-type.automower.start.description = Start for a duration in minutes
channel-type.automower.start.description = Start the Automower® for the given duration, overriding the schedule
channel-type.automower.startInWorkArea.label = Start in Work Area
channel-type.automower.startInWorkArea.description = Start the Automower® in the given Work Area, overriding the schedule. The Automower® will continue forever
channel-type.automower.statCuttingBladeUsageTimeType.label = Cutting Blade Usage Time
channel-type.automower.statCuttingBladeUsageTimeType.description = The time since the last reset of the cutting blade usage counter
channel-type.automower.statDownTimeType.label = Down Time
channel-type.automower.statDownTimeType.description = The time the mower has been disconnected from the cloud
channel-type.automower.statDownTimeType.state.pattern = %1$tHh:%1$tMm:%1$tSs
channel-type.automower.statDownTimeType.description = The time the Automower® has been disconnected from the cloud
channel-type.automower.statDownTimeType.state.pattern = %1$tdd:%1$tHh:%1$tMm:%1$tSs
channel-type.automower.statNumberOfChargingCyclesType.label = Number of Charging Cycles
channel-type.automower.statNumberOfChargingCyclesType.description = Number of charging cycles
channel-type.automower.statNumberOfCollisionsType.label = Number of Collisions
channel-type.automower.statNumberOfCollisionsType.description = The total number of collisions
channel-type.automower.statNumberOfCollisionsType.description = Total number of collisions
channel-type.automower.statTotalChargingTimeType.label = Total Charging Time
channel-type.automower.statTotalChargingTimeType.description = Total charging time
channel-type.automower.statTotalCuttingPercentType.label = Relative Total Cutting Time
@@ -172,8 +168,8 @@ channel-type.automower.statTotalSearchingPercentType.description = The total sea
channel-type.automower.statTotalSearchingTimeType.label = Total Searching Time
channel-type.automower.statTotalSearchingTimeType.description = The total searching time
channel-type.automower.statUpTimeType.label = Up Time
channel-type.automower.statUpTimeType.description = The time the mower has been connected to the cloud
channel-type.automower.statUpTimeType.state.pattern = %1$tHh:%1$tMm:%1$tSs
channel-type.automower.statUpTimeType.description = The time the Automower® has been connected to the cloud
channel-type.automower.statUpTimeType.state.pattern = %1$tdd:%1$tHh:%1$tMm:%1$tSs
channel-type.automower.stateType.label = State
channel-type.automower.stateType.description = The current state
channel-type.automower.stateType.state.option.UNKNOWN = Unknown
@@ -181,7 +177,7 @@ channel-type.automower.stateType.state.option.NOT_APPLICABLE = N/A
channel-type.automower.stateType.state.option.PAUSED = Paused by user
channel-type.automower.stateType.state.option.IN_OPERATION = Working
channel-type.automower.stateType.state.option.WAIT_UPDATING = Downloading new firmware
channel-type.automower.stateType.state.option.WAIT_POWER_UP = Booting mower
channel-type.automower.stateType.state.option.WAIT_POWER_UP = Booting Automower®
channel-type.automower.stateType.state.option.RESTRICTED_NONE = No restricted reason
channel-type.automower.stateType.state.option.RESTRICTED_WEEK_SCHEDULE = Restricted by week schedule
channel-type.automower.stateType.state.option.RESTRICTED_PARK_OVERRIDE = Forced to park
@@ -189,7 +185,7 @@ channel-type.automower.stateType.state.option.RESTRICTED_SENSOR = Restricted by
channel-type.automower.stateType.state.option.RESTRICTED_DAILY_LIMIT = Restricted by daily limit
channel-type.automower.stateType.state.option.RESTRICTED_FOTA = Restricted by FOTA transfer
channel-type.automower.stateType.state.option.RESTRICTED_FROST = Restricted by frost sensor
channel-type.automower.stateType.state.option.RESTRICTED_ALL_WORK_AREAS_COMPLETED = Restricted: All work areas completed
channel-type.automower.stateType.state.option.RESTRICTED_ALL_WORK_AREAS_COMPLETED = Restricted: All Work Areas completed
channel-type.automower.stateType.state.option.RESTRICTED_EXTERNAL = Restricted by external reason
channel-type.automower.stateType.state.option.OFF = Off
channel-type.automower.stateType.state.option.STOPPED = Stopped: Manual intervention required
@@ -197,29 +193,47 @@ channel-type.automower.stateType.state.option.ERROR = Error
channel-type.automower.stateType.state.option.FATAL_ERROR = Fatal error
channel-type.automower.stateType.state.option.ERROR_AT_POWER_UP = Boot error
channel-type.automower.workAreaIdType.label = Work Area Id
channel-type.automower.workAreaIdType.description = Id of the active work area
channel-type.automower.workAreaIdType.description = Id of the active Work Area
channel-type.automower.workAreaType.label = Work Area Name
channel-type.automower.workAreaType.description = Name of the active work area
channel-type.automower.workAreaType.description = Name of the active Work Area
channel-type.automower.workareaCuttingHeightType.label = Cutting Height
channel-type.automower.workareaCuttingHeightType.description = Cutting height of the work area, range: 0-100
channel-type.automower.workareaCuttingHeightType.description = Cutting height of the Work Area, range: 0-100
channel-type.automower.workareaEnabledType.label = Work Area Enabled
channel-type.automower.workareaEnabledType.description = If the work area is enabled or disabled
channel-type.automower.workareaEnabledType.description = If the Work Area is enabled or disabled
channel-type.automower.workareaIdType.label = Work Area Id
channel-type.automower.workareaIdType.description = Id of the Work Area
channel-type.automower.workareaLastTimeCompletedType.label = Last Time Completed
channel-type.automower.workareaLastTimeCompletedType.description = Timestamp when the work area was last completed. EPOS mowers and systematic mowing work areas only.
channel-type.automower.workareaLastTimeCompletedType.description = Timestamp when the Work Area was last completed. EPOS Automower® and systematic mowing Work Areas only
channel-type.automower.workareaNameType.label = Work Area Name
channel-type.automower.workareaNameType.description = Name of the work area
channel-type.automower.workareaNameType.description = Name of the Work Area
channel-type.automower.workareaProgressType.label = Work Area Progress
channel-type.automower.workareaProgressType.description = The progress on a work area. EPOS mowers and systematic mowing work areas only.
channel-type.automower.workareaProgressType.description = The progress on a Work Area. EPOS Automower® and systematic mowing Work Areas only
channel-type.automower.zoneDirtyType.label = Stayout Zones Dirty Flag
channel-type.automower.zoneDirtyType.description = If the stay-out zones are synchronized with the Husqvarna cloud. If the map is dirty you can not enable or disable a stay-out zone.
channel-type.automower.zoneDirtyType.description = If the stay-out zones are synchronized with the Husqvarna cloud. If the map is dirty you can not enable or disable a stay-out zone
channel-type.automower.zoneEnabledType.label = Stayout Zone Enabled
channel-type.automower.zoneEnabledType.description = If the Stayout zone is enabled, the Automower® will not access the zone
channel-type.automower.zoneEnabledType.description = If the stay-out zone is enabled, the Automower® will not access the zone
channel-type.automower.zoneIdType.label = Stayout Zone Id
channel-type.automower.zoneIdType.description = Id of the Stayout zone
channel-type.automower.zoneIdType.description = Id of the stay-out zone
channel-type.automower.zoneNameType.label = Stayout Zone Name
channel-type.automower.zoneNameType.description = The name of the Stayout zone
channel-type.automower.zoneNameType.description = The name of the stay-out zone
# channel types
channel-type.automower.settingHeadlightModeType.description = Headlight Mode
# thing types config
thing-type.config.automower.automower.pollingInterval.label = Polling Interval
thing-type.config.automower.automower.pollingInterval.description = How often the current Automower® state should be polled
# channel group types
channel-group-type.automower.positionsType.label = Positions
# channel types
channel-type.automower.lastPositionType.label = Last GPS Position
channel-type.automower.lastPositionType.description = Last GPS position of the mower
# channel types
@@ -238,7 +252,7 @@ channel-type.automower.plannerRestrictedReasonType.state.option.SENSOR = Restric
channel-type.automower.plannerRestrictedReasonType.state.option.DAILY_LIMIT = Restricted by daily limit
channel-type.automower.plannerRestrictedReasonType.state.option.FOTA = Restricted by FOTA transfer
channel-type.automower.plannerRestrictedReasonType.state.option.FROST = Restricted by frost sensor
channel-type.automower.plannerRestrictedReasonType.state.option.ALL_WORK_AREAS_COMPLETED = Restricted: All work areas completed
channel-type.automower.plannerRestrictedReasonType.state.option.ALL_WORK_AREAS_COMPLETED = Restricted: All Work Areas completed
channel-type.automower.plannerRestrictedReasonType.state.option.EXTERNAL = Restricted by external reason
# channel types
@@ -277,78 +291,78 @@ channel-type.automower.calendarTasksType.description = The channel providing a J
# actions
action-input-duration-label = duration
action-input-duration-desc = The duration of the automower command in minutes
action-park-label = park the automower
action-park-desc = Parks the automower for a defined amount of time, overriding its schedule.
action-input-duration-min-label = duration (min)
action-input-duration-min-desc = The duration of the Automower® command in minutes
action-input-duration-min-opt-label = duration (min)
action-input-duration-min-opt-desc = The duration of the Automower® command in minutes. If duration is skipped the Automower® will continue forever
action-input-work-area-id-label = work area id
action-input-work-area-id-desc = Id of Work Area
action-park-label = park
action-park-desc = Park the Automower® for the given duration (minutes), overriding the schedule
action-parkuntilfurthernotice-label = park until further notice
action-parkuntilfurthernotice-desc = Parks the automower until the schedule is resumed.
action-parkuntilfurthernotice-desc = Park the Automower® until it is started again by the start action/command or the schedule gets resumed
action-parkuntilnextschedule-label = park until the next schedule
action-parkuntilnextschedule-desc = Parks the automower until the next schedule.
action-pause-label = pause the automower
action-pause-desc = Pauses the automower wherever it currently is.
action-parkuntilnextschedule-desc = Park the Automower®, fully charge it and start afterwards according to the schedule
action-pause-label = pause
action-pause-desc = Pause the Automower® at the current location until manual resume
action-resumeschedule-label = resume the schedule
action-resumeschedule-desc = Resumes the schedule for the automower.
action-start-label = start the automower
action-start-desc = Starts the automower for a defined amount of time, overriding its schedule.
action-resumeschedule-desc = Resume the schedule of the Automower®
action-start-label = start
action-start-desc = Start the Automower® for the given duration (minutes), overriding the schedule
action-start-in-work-area-label = start in work area
action-start-in-work-area-desc = Start the Automower® in the given Work Area for the given duration (minutes), overriding the schedule
action-confirm-error-label = confirm error
action-confirm-error-desc = Confirm current non fatal error.
action-confirm-error-desc = Confirm current non fatal error
action-reset-cutting-blade-usage-time-label = reset cutting blade usage time
action-reset-cutting-blade-usage-time-desc = Reset the cutting blade usage time.
action-set-settings-label = update automower settings
action-set-settings-desc = Sends a set of Settings to the automower.
action-reset-cutting-blade-usage-time-desc = Reset the cutting blade usage time
action-set-settings-label = update settings
action-set-settings-desc = Sends a set of Settings to the Automower®
action-input-cutting-height-label = cutting height
action-input-cutting-height-desc = Prescaled cutting height, Range: 1-9.
action-input-cutting-height-desc = Prescaled cutting height, Range: 1-9
action-input-headlight-mode-label = headlight mode
action-input-headlight-mode-desc = Headlight Mode (ALWAYS_ON, ALWAYS_OFF, EVENING_ONLY, EVENING_AND_NIGHT).
action-input-headlight-mode-desc = Headlight Mode (ALWAYS_ON, ALWAYS_OFF, EVENING_ONLY, EVENING_AND_NIGHT)
action-set-work-area-label = update work area configuration
action-set-work-area-desc = Update the work area configuration of the specified area.
action-input-workarea-id-label = work area id
action-input-workarea-id-desc = Id of WorkArea to be updated.
action-set-work-area-desc = Update the Work Area configuration
action-input-enable-label = enable
action-input-enable-desc = Enable or disable WorkArea.
action-input-enable-desc = Enable or disable Work Area / stay-out zone
action-input-cutting-height-label = cutting height
action-input-cutting-height-desc = Cutting height of the WorkArea.
action-set-stayoutzone-label = update stayout zone configuration
action-set-stayoutzone-desc = Update the stayout zone configuration of the specified zone.
action-input-cutting-height-desc = Cutting height of the Work Area, range: 0-100
action-set-stayoutzone-label = update stay-out zone configuration
action-set-stayoutzone-desc = Update the stay-out zone configuration of the specified zone
action-input-zone-id-label = zone id
action-input-zone-id-desc = Id of the stayout zone to be updated.
action-input-enable-label = enable
action-input-enable-desc = Enable or disable stayout zone.
action-input-zone-id-desc = Id of the stay-out zone to be updated
action-set-calendartask-label = update calendar task configuration
action-set-calendartask-desc = Update the calendar task configuration of the mower or specified work area.
action-input-workarea-id-label = work area id
action-input-workarea-id-desc = Id of WorkArea where the calendar task configuration shall be updated (null if mower doesn't support work areas).
action-set-calendartask-desc = Update the calendar task configuration of the Automower® or specified Work Area
action-input-start-label = start
action-input-start-desc = Start time relative to midnight (minutes).
action-input-start-desc = Start time relative to midnight (minutes)
action-input-duration-label = duration
action-input-duration-desc = Duration time (minutes).
action-input-duration-desc = Duration time (minutes)
action-input-monday-label = monday[]
action-input-monday-desc = Enabled on Mondays (array with the number of calendar tasks).
action-input-monday-desc = Enabled on Mondays (array with the number of calendar tasks)
action-input-tuesday-label = tuesday[]
action-input-tuesday-desc = Enabled on Tuesdays (array with the number of calendar tasks).
action-input-tuesday-desc = Enabled on Tuesdays (array with the number of calendar tasks)
action-input-wednesday-label = wednesday[]
action-input-wednesday-desc = Enabled on Wednesdays (array with the number of calendar tasks).
action-input-wednesday-desc = Enabled on Wednesdays (array with the number of calendar tasks)
action-input-thursday-label = thursday[]
action-input-thursday-desc = Enabled on Thursdays (array with the number of calendar tasks).
action-input-thursday-desc = Enabled on Thursdays (array with the number of calendar tasks)
action-input-friday-label = friday[]
action-input-friday-desc = Enabled on Fridays (array with the number of calendar tasks).
action-input-friday-desc = Enabled on Fridays (array with the number of calendar tasks)
action-input-saturday-label = saturday
action-input-saturday-desc = Enabled on Saturdays (array with the number of calendar tasks).
action-input-saturday-desc = Enabled on Saturdays (array with the number of calendar tasks)
action-input-sunday-label = sunday
action-input-sunday-desc = Enabled on Sunday (array with the number of calendar tasks).
action-input-sunday-desc = Enabled on Sunday (array with the number of calendar tasks)
action-poll-label = poll
action-poll-desc = Poll mower status update from the cloud
action-poll-desc = Poll Automower® status update from the cloud
# status messages
comm-error-httpclient-init-failed = Unable to initialize http client
comm-error-query-mowers-failed = Unable to query registered mowers
comm-error-query-mower-failed = Unable to query the automower status
comm-error-send-mower-command-failed = Unable to send automower command
comm-error-mower-not-connected-to-cloud = Automower not connected to the cloud
conf-error-no-app-key = Cannot connect to Automower bridge as no app key is available in the configuration
conf-error-no-app-secret = Cannot connect to Automower bridge as no app secret is available in the configuration
comm-error-query-mowers-failed = Unable to query registered Automower®
comm-error-query-mower-failed = Unable to query the Automower® status
comm-error-send-mower-command-failed = Unable to send Automower® command
comm-error-mower-not-connected-to-cloud = Automower® not connected to the cloud
conf-error-no-app-key = Cannot connect to Automower® bridge as no app key is available in the configuration
conf-error-no-app-secret = Cannot connect to Automower® bridge as no app secret is available in the configuration
conf-error-invalid-polling-interval = Invalid polling interval specified. The polling interval has to be >= 1
conf-error-no-mower-id = No Automower ID specified. Unable to communicate with the mower without an ID
conf-error-no-bridge = No valid bridge for the automower is available
conf-error-no-mower-id = No Automower® ID specified. Unable to communicate with the Automower® without an ID
conf-error-no-bridge = No valid bridge for the Automower® is available
@@ -6,30 +6,31 @@
<!-- Automower Bridge Thing Type -->
<bridge-type id="bridge">
<label>Automower Connect Bridge</label>
<description>The bridge to communicate with the Automower Connect API</description>
<label>Automower® Connect Bridge</label>
<description>The bridge to communicate with the Automower® Connect API</description>
<semantic-equipment-tag>WebService</semantic-equipment-tag>
<config-description>
<parameter name="appKey" type="text" required="true">
<label>Application Key</label>
<description>The Application Key is required to communicate with the Automower Connect API at
https://developer.husqvarnagroup.cloud/. It can be obtained by
registering an Application on the Husqvarna Website.
This application also needs to be connected to the
"Authentication API" and the "Automower Connect API"</description>
<description>The Application Key is required to communicate with the Automower® Connect API.
It can be obtained by
registering an Application on https://developer.husqvarnagroup.cloud/.
This application needs to be connected to the
"Authentication API" and the "Automower® Connect
API". https://developer.husqvarnagroup.cloud/docs/getting-started
</description>
</parameter>
<parameter name="appSecret" type="text" required="true">
<label>Application Secret</label>
<description>The Application Secret is required to communicate with the Automower Connect API at
https://developer.husqvarnagroup.cloud/. It can be obtained by
registering an Application on the Husqvarna Website.</description>
<description>The Application Secret is required to communicate with the Automower® Connect API. See "Application
Key" for further details</description>
</parameter>
<parameter name="pollingInterval" type="integer" required="false" unit="s">
<label>Polling Interval</label>
<default>3600</default>
<description>How often the available automowers should be queried in seconds</description>
<default>300</default>
<description>How often the current Automower® states should be polled using REST API</description>
</parameter>
</config-description>
@@ -41,7 +42,7 @@
<bridge-type-ref id="bridge"/>
</supported-bridge-type-refs>
<label>Automower</label>
<label>Automower®</label>
<description>An automatic lawn mower</description>
<semantic-equipment-tag>LawnMower</semantic-equipment-tag>
@@ -50,7 +51,6 @@
<channel-group id="setting" typeId="settingsType"/>
<channel-group id="statistic" typeId="statisticsType"/>
<channel-group id="calendartask" typeId="calendarTasksType"/>
<channel-group id="position" typeId="positionsType"/>
<channel-group id="stayoutzone" typeId="stayoutZonesType"/>
<channel-group id="workarea" typeId="workAreasType"/>
<channel-group id="command" typeId="commandsType"/>
@@ -67,22 +67,18 @@
<property name="mowerHasPosition">N/A</property>
<property name="mowerHasStayOutZones">N/A</property>
<property name="mowerHasWorkAreas">N/A</property>
<property name="thingTypeVersion">1</property>
<property name="thingTypeVersion">2</property>
</properties>
<config-description>
<parameter name="mowerId" type="text" required="true">
<label>Automower Id</label>
<description>The Id of an automower as used by the Automower Connect Api to identify a mower</description>
</parameter>
<parameter name="pollingInterval" type="integer" required="false" unit="s">
<label>Polling Interval</label>
<default>600</default>
<description>How often the current automower state should be polled in seconds</description>
<label>Automower® Id</label>
<description>The Id of an Automower® as used by the Automower® Connect API to identify a Automower®. This is
automatically filled when the thing is discovered.</description>
</parameter>
<parameter name="mowerZoneId" type="text" required="false">
<label>TimeZone</label>
<description>Time zone of the mower (e.g. Europe/Berlin)</description>
<description>Time zone of the Automower® (e.g. Europe/Berlin). Default is the time zone configured in openHAB</description>
</parameter>
</config-description>
</thing-type>
@@ -114,6 +110,9 @@
<channel id="override-action" typeId="overrideActionType"/>
<channel id="restricted-reason" typeId="restrictedReasonType"/>
<channel id="external-reason" typeId="externalReasonType"/>
<!-- channels created on demand
<channel id="position" typeId="positionType"/>
-->
</channels>
</channel-group-type>
@@ -168,18 +167,6 @@
-->
</channel-group-type>
<!-- Positions -->
<channel-group-type id="positionsType">
<label>Positions</label>
<!-- channels created on demand
<channels>
<channel id="last" typeId="lastPositionType"/>
<channel id="<xx>" typeId="positionType"/>
...
</channels>
-->
</channel-group-type>
<!-- Stayout Zones -->
<channel-group-type id="stayoutZonesType">
<label>Stayout Zones</label>
@@ -213,16 +200,13 @@
<!-- Messages -->
<channel-group-type id="messagesType">
<label>Messages</label>
<!-- channels created dynamically (1...50)
<channels>
<channel id="<xx>-time" typeId="messageTimeType"/>
<channel id="<xx>-code" typeId="messageCodeType"/>
<channel id="<xx>-text" typeId="messageType"/>
<channel id="<xx>-severity" typeId="messageSeverityType"/>
<channel id="<xx>-position" typeId="messagePositionType"/>
...
</channels>
-->
<channels>
<channel id="msg-timestamp" typeId="messageTimeType"/>
<channel id="msg-code" typeId="messageCodeType"/>
<channel id="msg-text" typeId="messageType"/>
<channel id="msg-severity" typeId="messageSeverityType"/>
<channel id="msg-gps-position" typeId="messagePositionType"/>
</channels>
</channel-group-type>
<!-- Commands -->
@@ -230,6 +214,7 @@
<label>Commands</label>
<channels>
<channel id="start" typeId="start"/>
<channel id="start_in_work_area" typeId="startInWorkArea"/>
<channel id="resume_schedule" typeId="resumeSchedule"/>
<channel id="pause" typeId="pause"/>
<channel id="park" typeId="park"/>
@@ -240,8 +225,8 @@
<channel-type id="nameType">
<item-type>String</item-type>
<label>Automower Name</label>
<description>The channel providing the automower name</description>
<label>Automower® Name</label>
<description>The name of the Automower®</description>
<state readOnly="true"/>
</channel-type>
@@ -285,7 +270,7 @@
<state readOnly="true">
<options>
<option value="NONE">No inactive reason</option>
<option value="PLANNING">The mower is planning a path or a work area</option>
<option value="PLANNING">The Automower® is planning a path or a Work Area</option>
<option value="SEARCHING_FOR_SATELLITES">Waiting for fix when using EPOS</option>
</options>
</state>
@@ -302,7 +287,7 @@
<option value="PAUSED">Paused by user</option>
<option value="IN_OPERATION">Working</option>
<option value="WAIT_UPDATING">Downloading new firmware</option>
<option value="WAIT_POWER_UP">Booting mower</option>
<option value="WAIT_POWER_UP">Booting Automower®</option>
<option value="RESTRICTED_NONE">No restricted reason</option>
<option value="RESTRICTED_WEEK_SCHEDULE">Restricted by week schedule</option>
<option value="RESTRICTED_PARK_OVERRIDE">Forced to park</option>
@@ -310,7 +295,7 @@
<option value="RESTRICTED_DAILY_LIMIT">Restricted by daily limit</option>
<option value="RESTRICTED_FOTA">Restricted by FOTA transfer</option>
<option value="RESTRICTED_FROST">Restricted by frost sensor</option>
<option value="RESTRICTED_ALL_WORK_AREAS_COMPLETED">Restricted: All work areas completed</option>
<option value="RESTRICTED_ALL_WORK_AREAS_COMPLETED">Restricted: All Work Areas completed</option>
<option value="RESTRICTED_EXTERNAL">Restricted by external reason</option>
<option value="OFF">Off</option>
<option value="STOPPED">Stopped: Manual intervention required</option>
@@ -324,21 +309,21 @@
<channel-type id="workAreaIdType" advanced="true">
<item-type>Number</item-type>
<label>Work Area Id</label>
<description>Id of the active work area</description>
<description>Id of the active Work Area</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="workAreaType">
<item-type>String</item-type>
<label>Work Area Name</label>
<description>Name of the active work area</description>
<description>Name of the active Work Area</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="lastUpdateType">
<item-type>DateTime</item-type>
<label>Last Update</label>
<description>The time when the mower sent the last update</description>
<description>The time when the Automower® sent the last update</description>
<state readOnly="true"/>
</channel-type>
@@ -352,13 +337,13 @@
<channel-type id="pollUpdateType" advanced="true">
<item-type>Switch</item-type>
<label>Poll Update</label>
<description>Poll mower status update from the cloud</description>
<description>Poll Automower® status update from the cloud</description>
</channel-type>
<channel-type id="batteryType">
<item-type>Number:Dimensionless</item-type>
<label>Battery</label>
<description>The battery level of the mower at the time of last update</description>
<description>The battery state of charge in percent</description>
<tags>
<tag>Measurement</tag>
<tag>Energy</tag>
@@ -369,48 +354,50 @@
<channel-type id="errorCodeType" advanced="true">
<item-type>Number</item-type>
<label>Error Code</label>
<description>The error code at the time of last update</description>
<description>The current error code</description>
</channel-type>
<channel-type id="errorMessageType">
<item-type>String</item-type>
<label>Error Message</label>
<description>The error message at the time of last update</description>
<description>The current error message</description>
</channel-type>
<channel-type id="errorTimestampType">
<item-type>DateTime</item-type>
<label>Error Time</label>
<description>The time when the error occurred</description>
<description>The time when the current error occurred</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="errorConfirmableType" advanced="true">
<item-type>Switch</item-type>
<label>Error Confirmable</label>
<description>If the mower has an Error Code this attribute states if the error is confirmable</description>
<description>If the Automower® has an error, this attribute states if the error is confirmable</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="nextStartType">
<item-type>DateTime</item-type>
<label>Next Auto Start</label>
<description>The channel providing the time for the next auto start. If the mower is charging then the value is the
estimated time when it will be leaving the charging station. If the mower is about to start now, the value is NULL.</description>
<label>Next Start</label>
<description>The time for the next scheduled start.
If the Automower® is charging then the value is the estimated time
when it will be leaving the charging station.
If the Automower® is about to start now, the value is NULL</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="overrideActionType">
<item-type>String</item-type>
<label>Override Action</label>
<description>The channel providing an action that overrides current planner operation.</description>
<description>The action that overrides the current planner operation</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="restrictedReasonType" advanced="true">
<item-type>String</item-type>
<label>Restricted Reason</label>
<description>The channel providing a reason that restrics current planner operation.</description>
<description>The reason that restrics the current planner operation</description>
<state readOnly="true">
<options>
<option value="NONE">No restricted reason</option>
@@ -420,7 +407,7 @@
<option value="DAILY_LIMIT">Restricted by daily limit</option>
<option value="FOTA">Restricted by FOTA transfer</option>
<option value="FROST">Restricted by frost sensor</option>
<option value="ALL_WORK_AREAS_COMPLETED">Restricted: All work areas completed</option>
<option value="ALL_WORK_AREAS_COMPLETED">Restricted: All Work Areas completed</option>
<option value="EXTERNAL">Restricted by external reason</option>
</options>
</state>
@@ -429,7 +416,15 @@
<channel-type id="externalReasonType" advanced="true">
<item-type>Number</item-type>
<label>External Reason</label>
<description>The channel providing an external reason that restrics current planner operation.</description>
<description>An external reason set by i.e. Google Assistant or Amazon Alexa that restrics the current planner
operation</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="positionType" advanced="false">
<item-type>Location</item-type>
<label>Last GPS Position</label>
<description>Last GPS position of the Automower®</description>
<state readOnly="true"/>
</channel-type>
@@ -442,7 +437,6 @@
<channel-type id="settingHeadlightModeType">
<item-type>String</item-type>
<label>Headlight Mode</label>
<description>Information about headlights</description>
</channel-type>
<channel-type id="statCuttingBladeUsageTimeType">
@@ -455,8 +449,8 @@
<channel-type id="statDownTimeType" advanced="true">
<item-type>Number:Time</item-type>
<label>Down Time</label>
<description>The time the mower has been disconnected from the cloud</description>
<state readOnly="true" pattern="%1$tHh:%1$tMm:%1$tSs"/>
<description>The time the Automower® has been disconnected from the cloud</description>
<state readOnly="true" pattern="%1$tdd:%1$tHh:%1$tMm:%1$tSs"/>
</channel-type>
<channel-type id="statNumberOfChargingCyclesType">
@@ -469,7 +463,7 @@
<channel-type id="statNumberOfCollisionsType">
<item-type>Number</item-type>
<label>Number of Collisions</label>
<description>The total number of collisions</description>
<description>Total number of collisions</description>
<state readOnly="true"/>
</channel-type>
@@ -525,22 +519,8 @@
<channel-type id="statUpTimeType" advanced="true">
<item-type>Number:Time</item-type>
<label>Up Time</label>
<description>The time the mower has been connected to the cloud</description>
<state readOnly="true" pattern="%1$tHh:%1$tMm:%1$tSs"/>
</channel-type>
<channel-type id="lastPositionType" advanced="false">
<item-type>Location</item-type>
<label>Last GPS Position</label>
<description>Last GPS position of the mower</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="positionType" advanced="true">
<item-type>Location</item-type>
<label>GPS Position</label>
<description>A waypoint of the mower's activity</description>
<state readOnly="true"/>
<description>The time the Automower® has been connected to the cloud</description>
<state readOnly="true" pattern="%1$tdd:%1$tHh:%1$tMm:%1$tSs"/>
</channel-type>
<!-- Calendar Tasks -->
@@ -603,28 +583,28 @@
<item-type>Switch</item-type>
<label>Stayout Zones Dirty Flag</label>
<description>If the stay-out zones are synchronized with the Husqvarna cloud. If the map is dirty you can not enable
or disable a stay-out zone.</description>
or disable a stay-out zone</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="zoneIdType" advanced="true">
<item-type>String</item-type>
<label>Stayout Zone Id</label>
<description>Id of the Stayout zone</description>
<description>Id of the stay-out zone</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="zoneNameType" advanced="true">
<item-type>String</item-type>
<label>Stayout Zone Name</label>
<description>The name of the Stayout zone</description>
<description>The name of the stay-out zone</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="zoneEnabledType" advanced="true">
<item-type>Switch</item-type>
<label>Stayout Zone Enabled</label>
<description>If the Stayout zone is enabled, the Automower® will not access the zone</description>
<description>If the stay-out zone is enabled, the Automower® will not access the zone</description>
</channel-type>
<!-- Work Areas -->
@@ -638,70 +618,70 @@
<channel-type id="workareaNameType" advanced="true">
<item-type>String</item-type>
<label>Work Area Name</label>
<description>Name of the work area</description>
<description>Name of the Work Area</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="workareaCuttingHeightType" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>Cutting Height</label>
<description>Cutting height of the work area, range: 0-100</description>
<description>Cutting height of the Work Area, range: 0-100</description>
<state pattern="%d %%"/>
</channel-type>
<channel-type id="workareaEnabledType" advanced="true">
<item-type>Switch</item-type>
<label>Work Area Enabled</label>
<description>If the work area is enabled or disabled</description>
<description>If the Work Area is enabled or disabled</description>
</channel-type>
<channel-type id="workareaProgressType" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>Work Area Progress</label>
<description>The progress on a work area. EPOS mowers and systematic mowing work areas only.</description>
<description>The progress on a Work Area. EPOS Automower® and systematic mowing Work Areas only</description>
<state readOnly="true" pattern="%d %%"/>
</channel-type>
<channel-type id="workareaLastTimeCompletedType" advanced="true">
<item-type>DateTime</item-type>
<label>Last Time Completed</label>
<description>Timestamp when the work area was last completed. EPOS mowers and systematic mowing work areas only.</description>
<description>Timestamp when the Work Area was last completed. EPOS Automower® and systematic mowing Work Areas only</description>
<state readOnly="true"/>
</channel-type>
<!-- Messages -->
<channel-type id="messageTimeType" advanced="true">
<item-type>DateTime</item-type>
<label>Timestamp</label>
<description>Timestamp when the event occurred</description>
<label>Last Error Time</label>
<description>The time when the last error occurred</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="messageCodeType" advanced="true">
<item-type>Number</item-type>
<label>Code</label>
<description>(Error) code of the event</description>
<label>Last Error Code</label>
<description>The last error code</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="messageType" advanced="true">
<item-type>String</item-type>
<label>Message</label>
<description>The Message</description>
<label>Last Error Message</label>
<description>The last error message</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="messageSeverityType" advanced="true">
<item-type>String</item-type>
<label>Severity</label>
<description>The severity of the event</description>
<label>Message Severity</label>
<description>The severity of the last error</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="messagePositionType" advanced="true">
<item-type>Location</item-type>
<label>GPS Position</label>
<description>GPS Position of the event (if available)</description>
<label>Message GPS Position</label>
<description>GPS position of the last event (if available)</description>
<state readOnly="true"/>
</channel-type>
@@ -709,38 +689,46 @@
<channel-type id="start">
<item-type>Number</item-type>
<label>Start with Duration</label>
<description>Start for a duration in minutes</description>
<description>Start the Automower® for the given duration, overriding the schedule</description>
<state pattern="%d min" min="1" max="1440" step="1"/>
</channel-type>
<channel-type id="startInWorkArea">
<item-type>Number</item-type>
<label>Start in Work Area</label>
<description>Start the Automower® in the given Work Area, overriding the schedule. The Automower® will continue
forever</description>
<state pattern="%d"/>
</channel-type>
<channel-type id="resumeSchedule">
<item-type>Switch</item-type>
<label>Resume Schedule</label>
<description>Resume schedule</description>
<description>Resume the schedule of the Automower®</description>
</channel-type>
<channel-type id="pause">
<item-type>Switch</item-type>
<label>Pause</label>
<description>Pause the mower now until manual resume</description>
<description>Pause the Automower® at the current location until manual resume</description>
</channel-type>
<channel-type id="park">
<item-type>Number</item-type>
<label>Park with Duration</label>
<description>Park for a duration in minutes</description>
<description>Park the Automower® for the given duration, overriding the schedule</description>
<state pattern="%d min" min="1" max="1440" step="1"/>
</channel-type>
<channel-type id="parkUntilNextSchedule">
<item-type>Switch</item-type>
<label>Park Until Next Schedule</label>
<description>Park until next schedule</description>
<description>Park the Automower®, fully charge it and start afterwards according to the schedule</description>
</channel-type>
<channel-type id="parkUntilFurtherNotice">
<item-type>Switch</item-type>
<label>Park and Pause the Schedule</label>
<description>Park and pause the mower schedule until manual resume</description>
<description>Park the Automower® until it is started again by the start action/command or the schedule gets resumed</description>
</channel-type>
</thing:thing-descriptions>
@@ -158,24 +158,352 @@
<remove-channel id="position50"/>
<!-- Commands -->
<update-channel id="start" groupIds="commands">
<update-channel id="start" groupIds="command">
<type>automower:start</type>
</update-channel>
<update-channel id="resume_schedule" groupIds="commands">
<update-channel id="resume_schedule" groupIds="command">
<type>automower:resumeSchedule</type>
</update-channel>
<update-channel id="pause" groupIds="commands">
<update-channel id="pause" groupIds="command">
<type>automower:pause</type>
</update-channel>
<update-channel id="park" groupIds="commands">
<update-channel id="park" groupIds="command">
<type>automower:park</type>
</update-channel>
<update-channel id="park_until_next_schedule" groupIds="commands">
<update-channel id="park_until_next_schedule" groupIds="command">
<type>automower:parkUntilNextSchedule</type>
</update-channel>
<update-channel id="park_until_further_notice" groupIds="commands">
<update-channel id="park_until_further_notice" groupIds="command">
<type>automower:parkUntilFurtherNotice</type>
</update-channel>
</instruction-set>
<instruction-set targetVersion="2">
<!-- Positions -->
<remove-channel id="position#last"/>
<remove-channel id="position#01-pos"/>
<remove-channel id="position#02-pos"/>
<remove-channel id="position#03-pos"/>
<remove-channel id="position#04-pos"/>
<remove-channel id="position#05-pos"/>
<remove-channel id="position#06-pos"/>
<remove-channel id="position#07-pos"/>
<remove-channel id="position#08-pos"/>
<remove-channel id="position#09-pos"/>
<remove-channel id="position#10-pos"/>
<remove-channel id="position#11-pos"/>
<remove-channel id="position#12-pos"/>
<remove-channel id="position#13-pos"/>
<remove-channel id="position#14-pos"/>
<remove-channel id="position#15-pos"/>
<remove-channel id="position#16-pos"/>
<remove-channel id="position#17-pos"/>
<remove-channel id="position#18-pos"/>
<remove-channel id="position#19-pos"/>
<remove-channel id="position#20-pos"/>
<remove-channel id="position#21-pos"/>
<remove-channel id="position#22-pos"/>
<remove-channel id="position#23-pos"/>
<remove-channel id="position#24-pos"/>
<remove-channel id="position#25-pos"/>
<remove-channel id="position#26-pos"/>
<remove-channel id="position#27-pos"/>
<remove-channel id="position#28-pos"/>
<remove-channel id="position#29-pos"/>
<remove-channel id="position#30-pos"/>
<remove-channel id="position#31-pos"/>
<remove-channel id="position#32-pos"/>
<remove-channel id="position#33-pos"/>
<remove-channel id="position#34-pos"/>
<remove-channel id="position#35-pos"/>
<remove-channel id="position#36-pos"/>
<remove-channel id="position#37-pos"/>
<remove-channel id="position#38-pos"/>
<remove-channel id="position#39-pos"/>
<remove-channel id="position#40-pos"/>
<remove-channel id="position#41-pos"/>
<remove-channel id="position#42-pos"/>
<remove-channel id="position#43-pos"/>
<remove-channel id="position#44-pos"/>
<remove-channel id="position#45-pos"/>
<remove-channel id="position#46-pos"/>
<remove-channel id="position#47-pos"/>
<remove-channel id="position#48-pos"/>
<remove-channel id="position#49-pos"/>
<remove-channel id="position#50-pos"/>
<!-- Messages -->
<remove-channel id="message#01-msg-timestamp"/>
<remove-channel id="message#01-msg-code"/>
<remove-channel id="message#01-msg-text"/>
<remove-channel id="message#01-msg-severity"/>
<remove-channel id="message#01-msg-gps-position"/>
<remove-channel id="message#02-msg-timestamp"/>
<remove-channel id="message#02-msg-code"/>
<remove-channel id="message#02-msg-text"/>
<remove-channel id="message#02-msg-severity"/>
<remove-channel id="message#02-msg-gps-position"/>
<remove-channel id="message#03-msg-timestamp"/>
<remove-channel id="message#03-msg-code"/>
<remove-channel id="message#03-msg-text"/>
<remove-channel id="message#03-msg-severity"/>
<remove-channel id="message#03-msg-gps-position"/>
<remove-channel id="message#04-msg-timestamp"/>
<remove-channel id="message#04-msg-code"/>
<remove-channel id="message#04-msg-text"/>
<remove-channel id="message#04-msg-severity"/>
<remove-channel id="message#04-msg-gps-position"/>
<remove-channel id="message#05-msg-timestamp"/>
<remove-channel id="message#05-msg-code"/>
<remove-channel id="message#05-msg-text"/>
<remove-channel id="message#05-msg-severity"/>
<remove-channel id="message#05-msg-gps-position"/>
<remove-channel id="message#06-msg-timestamp"/>
<remove-channel id="message#06-msg-code"/>
<remove-channel id="message#06-msg-text"/>
<remove-channel id="message#06-msg-severity"/>
<remove-channel id="message#06-msg-gps-position"/>
<remove-channel id="message#07-msg-timestamp"/>
<remove-channel id="message#07-msg-code"/>
<remove-channel id="message#07-msg-text"/>
<remove-channel id="message#07-msg-severity"/>
<remove-channel id="message#07-msg-gps-position"/>
<remove-channel id="message#08-msg-timestamp"/>
<remove-channel id="message#08-msg-code"/>
<remove-channel id="message#08-msg-text"/>
<remove-channel id="message#08-msg-severity"/>
<remove-channel id="message#08-msg-gps-position"/>
<remove-channel id="message#09-msg-timestamp"/>
<remove-channel id="message#09-msg-code"/>
<remove-channel id="message#09-msg-text"/>
<remove-channel id="message#09-msg-severity"/>
<remove-channel id="message#09-msg-gps-position"/>
<remove-channel id="message#10-msg-timestamp"/>
<remove-channel id="message#10-msg-code"/>
<remove-channel id="message#10-msg-text"/>
<remove-channel id="message#10-msg-severity"/>
<remove-channel id="message#10-msg-gps-position"/>
<remove-channel id="message#11-msg-timestamp"/>
<remove-channel id="message#11-msg-code"/>
<remove-channel id="message#11-msg-text"/>
<remove-channel id="message#11-msg-severity"/>
<remove-channel id="message#11-msg-gps-position"/>
<remove-channel id="message#12-msg-timestamp"/>
<remove-channel id="message#12-msg-code"/>
<remove-channel id="message#12-msg-text"/>
<remove-channel id="message#12-msg-severity"/>
<remove-channel id="message#12-msg-gps-position"/>
<remove-channel id="message#13-msg-timestamp"/>
<remove-channel id="message#13-msg-code"/>
<remove-channel id="message#13-msg-text"/>
<remove-channel id="message#13-msg-severity"/>
<remove-channel id="message#13-msg-gps-position"/>
<remove-channel id="message#14-msg-timestamp"/>
<remove-channel id="message#14-msg-code"/>
<remove-channel id="message#14-msg-text"/>
<remove-channel id="message#14-msg-severity"/>
<remove-channel id="message#14-msg-gps-position"/>
<remove-channel id="message#15-msg-timestamp"/>
<remove-channel id="message#15-msg-code"/>
<remove-channel id="message#15-msg-text"/>
<remove-channel id="message#15-msg-severity"/>
<remove-channel id="message#15-msg-gps-position"/>
<remove-channel id="message#16-msg-timestamp"/>
<remove-channel id="message#16-msg-code"/>
<remove-channel id="message#16-msg-text"/>
<remove-channel id="message#16-msg-severity"/>
<remove-channel id="message#16-msg-gps-position"/>
<remove-channel id="message#17-msg-timestamp"/>
<remove-channel id="message#17-msg-code"/>
<remove-channel id="message#17-msg-text"/>
<remove-channel id="message#17-msg-severity"/>
<remove-channel id="message#17-msg-gps-position"/>
<remove-channel id="message#18-msg-timestamp"/>
<remove-channel id="message#18-msg-code"/>
<remove-channel id="message#18-msg-text"/>
<remove-channel id="message#18-msg-severity"/>
<remove-channel id="message#18-msg-gps-position"/>
<remove-channel id="message#19-msg-timestamp"/>
<remove-channel id="message#19-msg-code"/>
<remove-channel id="message#19-msg-text"/>
<remove-channel id="message#19-msg-severity"/>
<remove-channel id="message#19-msg-gps-position"/>
<remove-channel id="message#20-msg-timestamp"/>
<remove-channel id="message#20-msg-code"/>
<remove-channel id="message#20-msg-text"/>
<remove-channel id="message#20-msg-severity"/>
<remove-channel id="message#20-msg-gps-position"/>
<remove-channel id="message#21-msg-timestamp"/>
<remove-channel id="message#21-msg-code"/>
<remove-channel id="message#21-msg-text"/>
<remove-channel id="message#21-msg-severity"/>
<remove-channel id="message#21-msg-gps-position"/>
<remove-channel id="message#22-msg-timestamp"/>
<remove-channel id="message#22-msg-code"/>
<remove-channel id="message#22-msg-text"/>
<remove-channel id="message#22-msg-severity"/>
<remove-channel id="message#22-msg-gps-position"/>
<remove-channel id="message#23-msg-timestamp"/>
<remove-channel id="message#23-msg-code"/>
<remove-channel id="message#23-msg-text"/>
<remove-channel id="message#23-msg-severity"/>
<remove-channel id="message#23-msg-gps-position"/>
<remove-channel id="message#24-msg-timestamp"/>
<remove-channel id="message#24-msg-code"/>
<remove-channel id="message#24-msg-text"/>
<remove-channel id="message#24-msg-severity"/>
<remove-channel id="message#24-msg-gps-position"/>
<remove-channel id="message#25-msg-timestamp"/>
<remove-channel id="message#25-msg-code"/>
<remove-channel id="message#25-msg-text"/>
<remove-channel id="message#25-msg-severity"/>
<remove-channel id="message#25-msg-gps-position"/>
<remove-channel id="message#26-msg-timestamp"/>
<remove-channel id="message#26-msg-code"/>
<remove-channel id="message#26-msg-text"/>
<remove-channel id="message#26-msg-severity"/>
<remove-channel id="message#26-msg-gps-position"/>
<remove-channel id="message#27-msg-timestamp"/>
<remove-channel id="message#27-msg-code"/>
<remove-channel id="message#27-msg-text"/>
<remove-channel id="message#27-msg-severity"/>
<remove-channel id="message#27-msg-gps-position"/>
<remove-channel id="message#28-msg-timestamp"/>
<remove-channel id="message#28-msg-code"/>
<remove-channel id="message#28-msg-text"/>
<remove-channel id="message#28-msg-severity"/>
<remove-channel id="message#28-msg-gps-position"/>
<remove-channel id="message#29-msg-timestamp"/>
<remove-channel id="message#29-msg-code"/>
<remove-channel id="message#29-msg-text"/>
<remove-channel id="message#29-msg-severity"/>
<remove-channel id="message#29-msg-gps-position"/>
<remove-channel id="message#30-msg-timestamp"/>
<remove-channel id="message#30-msg-code"/>
<remove-channel id="message#30-msg-text"/>
<remove-channel id="message#30-msg-severity"/>
<remove-channel id="message#30-msg-gps-position"/>
<remove-channel id="message#31-msg-timestamp"/>
<remove-channel id="message#31-msg-code"/>
<remove-channel id="message#31-msg-text"/>
<remove-channel id="message#31-msg-severity"/>
<remove-channel id="message#31-msg-gps-position"/>
<remove-channel id="message#32-msg-timestamp"/>
<remove-channel id="message#32-msg-code"/>
<remove-channel id="message#32-msg-text"/>
<remove-channel id="message#32-msg-severity"/>
<remove-channel id="message#32-msg-gps-position"/>
<remove-channel id="message#33-msg-timestamp"/>
<remove-channel id="message#33-msg-code"/>
<remove-channel id="message#33-msg-text"/>
<remove-channel id="message#33-msg-severity"/>
<remove-channel id="message#33-msg-gps-position"/>
<remove-channel id="message#34-msg-timestamp"/>
<remove-channel id="message#34-msg-code"/>
<remove-channel id="message#34-msg-text"/>
<remove-channel id="message#34-msg-severity"/>
<remove-channel id="message#34-msg-gps-position"/>
<remove-channel id="message#35-msg-timestamp"/>
<remove-channel id="message#35-msg-code"/>
<remove-channel id="message#35-msg-text"/>
<remove-channel id="message#35-msg-severity"/>
<remove-channel id="message#35-msg-gps-position"/>
<remove-channel id="message#36-msg-timestamp"/>
<remove-channel id="message#36-msg-code"/>
<remove-channel id="message#36-msg-text"/>
<remove-channel id="message#36-msg-severity"/>
<remove-channel id="message#36-msg-gps-position"/>
<remove-channel id="message#37-msg-timestamp"/>
<remove-channel id="message#37-msg-code"/>
<remove-channel id="message#37-msg-text"/>
<remove-channel id="message#37-msg-severity"/>
<remove-channel id="message#37-msg-gps-position"/>
<remove-channel id="message#38-msg-timestamp"/>
<remove-channel id="message#38-msg-code"/>
<remove-channel id="message#38-msg-text"/>
<remove-channel id="message#38-msg-severity"/>
<remove-channel id="message#38-msg-gps-position"/>
<remove-channel id="message#39-msg-timestamp"/>
<remove-channel id="message#39-msg-code"/>
<remove-channel id="message#39-msg-text"/>
<remove-channel id="message#39-msg-severity"/>
<remove-channel id="message#39-msg-gps-position"/>
<remove-channel id="message#40-msg-timestamp"/>
<remove-channel id="message#40-msg-code"/>
<remove-channel id="message#40-msg-text"/>
<remove-channel id="message#40-msg-severity"/>
<remove-channel id="message#40-msg-gps-position"/>
<remove-channel id="message#41-msg-timestamp"/>
<remove-channel id="message#41-msg-code"/>
<remove-channel id="message#41-msg-text"/>
<remove-channel id="message#41-msg-severity"/>
<remove-channel id="message#41-msg-gps-position"/>
<remove-channel id="message#42-msg-timestamp"/>
<remove-channel id="message#42-msg-code"/>
<remove-channel id="message#42-msg-text"/>
<remove-channel id="message#42-msg-severity"/>
<remove-channel id="message#42-msg-gps-position"/>
<remove-channel id="message#43-msg-timestamp"/>
<remove-channel id="message#43-msg-code"/>
<remove-channel id="message#43-msg-text"/>
<remove-channel id="message#43-msg-severity"/>
<remove-channel id="message#43-msg-gps-position"/>
<remove-channel id="message#44-msg-timestamp"/>
<remove-channel id="message#44-msg-code"/>
<remove-channel id="message#44-msg-text"/>
<remove-channel id="message#44-msg-severity"/>
<remove-channel id="message#44-msg-gps-position"/>
<remove-channel id="message#45-msg-timestamp"/>
<remove-channel id="message#45-msg-code"/>
<remove-channel id="message#45-msg-text"/>
<remove-channel id="message#45-msg-severity"/>
<remove-channel id="message#45-msg-gps-position"/>
<remove-channel id="message#46-msg-timestamp"/>
<remove-channel id="message#46-msg-code"/>
<remove-channel id="message#46-msg-text"/>
<remove-channel id="message#46-msg-severity"/>
<remove-channel id="message#46-msg-gps-position"/>
<remove-channel id="message#47-msg-timestamp"/>
<remove-channel id="message#47-msg-code"/>
<remove-channel id="message#47-msg-text"/>
<remove-channel id="message#47-msg-severity"/>
<remove-channel id="message#47-msg-gps-position"/>
<remove-channel id="message#48-msg-timestamp"/>
<remove-channel id="message#48-msg-code"/>
<remove-channel id="message#48-msg-text"/>
<remove-channel id="message#48-msg-severity"/>
<remove-channel id="message#48-msg-gps-position"/>
<remove-channel id="message#49-msg-timestamp"/>
<remove-channel id="message#49-msg-code"/>
<remove-channel id="message#49-msg-text"/>
<remove-channel id="message#49-msg-severity"/>
<remove-channel id="message#49-msg-gps-position"/>
<remove-channel id="message#50-msg-timestamp"/>
<remove-channel id="message#50-msg-code"/>
<remove-channel id="message#50-msg-text"/>
<remove-channel id="message#50-msg-severity"/>
<remove-channel id="message#50-msg-gps-position"/>
<add-channel id="msg-timestamp" groupIds="message">
<type>automower:messageTimeType</type>
</add-channel>
<add-channel id="msg-code" groupIds="message">
<type>automower:messageCodeType</type>
</add-channel>
<add-channel id="msg-text" groupIds="message">
<type>automower:messageType</type>
</add-channel>
<add-channel id="msg-severity" groupIds="message">
<type>automower:messageSeverityType</type>
</add-channel>
<add-channel id="msg-gps-position" groupIds="message">
<type>automower:messagePositionType</type>
</add-channel>
<!-- Commands -->
<add-channel id="start_in_work_area" groupIds="command">
<type>automower:startInWorkArea</type>
</add-channel>
</instruction-set>
</thing-type>
</update:update-descriptions>