Skip to content

Commit

Permalink
Merge pull request #543 from Thomas55555/websocket_v2
Browse files Browse the repository at this point in the history
Add websocket V2 support
  • Loading branch information
Thomas55555 authored Dec 11, 2024
2 parents dfbb1f2 + 7624188 commit 1e5c945
Show file tree
Hide file tree
Showing 12 changed files with 384 additions and 11 deletions.
15 changes: 14 additions & 1 deletion src/aioautomower/const.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""The constants for aioautomower."""

from enum import IntEnum
from enum import IntEnum, StrEnum

API_BASE_URL = "https://api.amc.husqvarna.dev/v1"
AUTH_API_BASE_URL = "https://api.authentication.husqvarnagroup.dev/v1"
Expand All @@ -23,6 +23,19 @@
WS_URL = "wss://ws.openapi.husqvarna.dev/v1"


class EventTypesV2(StrEnum):
"""Websocket events from websocket V2."""

BATTERY = "battery-event-v2"
CALENDAR = "calendar-event-v2"
CUTTING_HEIGHT = "cuttingHeight-event-v2"
HEADLIGHTS = "headLights-event-v2"
MESSAGES = "messages-event-v2"
MOWER = "mower-event-v2"
PLANNER = "planner-event-v2"
POSITIONS = "positions-event-v2"


class DayOfWeek(IntEnum):
"""Day of the week."""

Expand Down
69 changes: 60 additions & 9 deletions src/aioautomower/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
import datetime
import logging
import zoneinfo
from collections.abc import Iterable, Mapping
from collections.abc import Iterable, Mapping, MutableMapping
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Literal

import tzlocal
from aiohttp import WSMessage, WSMsgType

from .auth import AbstractAuth
from .const import EVENT_TYPES, REST_POLL_CYCLE
from .const import EVENT_TYPES, REST_POLL_CYCLE, EventTypesV2
from .exceptions import (
FeatureNotSupportedException,
NoDataAvailableException,
Expand Down Expand Up @@ -475,7 +475,9 @@ def _handle_text_message(self, msg: WSMessage) -> None:
if msg.data:
msg_dict = msg.json()
if "type" in msg_dict:
if msg_dict["type"] in EVENT_TYPES:
if msg_dict["type"] in set(EVENT_TYPES) | {
event.value for event in EventTypesV2
}:
if msg_dict["type"] == "settings-event":
copy = dict(msg_dict)
msg_dict = self.add_settigs_tree(copy)
Expand Down Expand Up @@ -526,22 +528,71 @@ async def get_status(self) -> dict[str, MowerAttributes]:
self.commands = _MowerCommands(self.auth, self.data, self.mower_tz)
return self.data

def _update_data(self, new_data) -> None:
"""Update internal data, with new data from websocket."""
def _update_data(self, new_data: Mapping[str, Any]) -> None:
"""Update internal data with new data from websocket."""
if self._data is None:
raise NoDataAvailableException

data = self._data["data"]

for mower in data:
if mower["type"] == "mower" and mower["id"] == new_data["id"]:
new_attributes: Mapping[Any, Any] = new_data["attributes"]
value: Mapping[Any, Any]
for attrib, value in new_attributes.items():
mower["attributes"][attrib] = value
self._process_event(mower, new_data)
break

self.data = mower_list_to_dictionary_dataclass(self._data, self.mower_tz)
self.commands = _MowerCommands(self.auth, self.data, self.mower_tz)
self._schedule_data_callbacks()

def _process_event(self, mower: dict, new_data: Mapping[str, Any]) -> None:
"""Process a specific event type."""
handlers = {
"cuttingHeight": self._handle_cutting_height_event,
"headLight": self._handle_headlight_event,
"position": self._handle_position_event,
}

attributes = new_data.get("attributes", {})
for key, handler in handlers.items():
if key in attributes:
handler(mower, attributes)
return

# General handling for other attributes
self._update_nested_dict(mower["attributes"], attributes)

def _handle_cutting_height_event(self, mower: dict, attributes: dict) -> None:
"""Handle cuttingHeight-specific updates."""
mower["attributes"]["settings"]["cuttingHeight"] = attributes["cuttingHeight"][
"height"
]

def _handle_headlight_event(self, mower: dict, attributes: dict) -> None:
"""Handle headLight-specific updates."""
mower["attributes"]["settings"]["headlight"]["mode"] = attributes["headLight"][
"mode"
]

def _handle_position_event(
self, mower: dict[str, dict[str, list[dict]]], attributes: dict[str, dict]
) -> None:
mower["attributes"]["positions"].insert(0, attributes["position"])

@staticmethod
def _update_nested_dict(
original: MutableMapping[Any, Any], updates: Mapping[Any, Any]
) -> None:
"""Recursively update a nested dictionary with new values."""
for key, value in updates.items():
if (
isinstance(value, dict)
and key in original
and isinstance(original[key], dict)
):
AutomowerSession._update_nested_dict(original[key], value)
else:
original[key] = value

async def _rest_task(self) -> None:
"""Poll data periodically via Rest."""
while True:
Expand Down
9 changes: 9 additions & 0 deletions tests/fixtures/events/battery_event.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"id": "c7233734-b219-4287-a173-08e3643f89f0",
"type": "battery-event-v2",
"attributes": {
"battery": {
"batteryPercent": 77
}
}
}
21 changes: 21 additions & 0 deletions tests/fixtures/events/calendar_event.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"id": "c7233734-b219-4287-a173-08e3643f89f0",
"type": "calendar-event-v2",
"attributes": {
"calendar": {
"tasks": [
{
"start": 420,
"duration": 780,
"monday": true,
"tuesday": true,
"wednesday": true,
"thursday": true,
"friday": true,
"saturday": false,
"sunday": false
}
]
}
}
}
22 changes: 22 additions & 0 deletions tests/fixtures/events/calendar_event_work_area.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"id": "c7233734-b219-4287-a173-08e3643f89f0",
"type": "calendar-event-v2",
"attributes": {
"calendar": {
"tasks": [
{
"start": 720,
"duration": 300,
"workAreaId": 78543,
"monday": true,
"tuesday": true,
"wednesday": true,
"thursday": true,
"friday": true,
"saturday": true,
"sunday": true
}
]
}
}
}
9 changes: 9 additions & 0 deletions tests/fixtures/events/cutting_height_event.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"id": "c7233734-b219-4287-a173-08e3643f89f0",
"type": "cuttingHeight-event-v2",
"attributes": {
"cuttingHeight": {
"height": 5
}
}
}
9 changes: 9 additions & 0 deletions tests/fixtures/events/headlights_event.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"id": "c7233734-b219-4287-a173-08e3643f89f0",
"type": "headLights-event-v2",
"attributes": {
"headLight": {
"mode": "ALWAYS_ON"
}
}
}
13 changes: 13 additions & 0 deletions tests/fixtures/events/messages_event.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"id": "c7233734-b219-4287-a173-08e3643f89f0",
"type": "messages-event-v2",
"attributes": {
"message": {
"time": 1728034996,
"code": 3,
"severity": "WARNING",
"latitude": 57.7086409,
"longitude": 14.1678988
}
}
}
16 changes: 16 additions & 0 deletions tests/fixtures/events/mower_event.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"id": "c7233734-b219-4287-a173-08e3643f89f0",
"type": "mower-event-v2",
"attributes": {
"mower": {
"mode": "MAIN_AREA",
"activity": "MOWING",
"inactiveReason": "NONE",
"state": "IN_OPERATION",
"errorCode": 0,
"isErrorConfirmable": false,
"workAreaId": "78555",
"errorCodeTimestamp": 0
}
}
}
14 changes: 14 additions & 0 deletions tests/fixtures/events/planner_event.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"id": "c7233734-b219-4287-a173-08e3643f89f0",
"type": "planner-event-v2",
"attributes": {
"planner": {
"nextStartTimestamp": 0,
"override": {
"action": "FORCE_MOW"
},
"restrictedReason": "PARK_OVERRIDE",
"externalReason": 7
}
}
}
10 changes: 10 additions & 0 deletions tests/fixtures/events/positions_event.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"id": "c7233734-b219-4287-a173-08e3643f89f0",
"type": "positions-event-v2",
"attributes": {
"position": {
"latitude": 57.70074,
"longitude": 14.4787133
}
}
}
Loading

0 comments on commit 1e5c945

Please sign in to comment.