Repository: plugwise/python-plugwise (language: python)

Issue:
Initial split domain structure
As per discussion on discourse:

Split functionality with 0.30 release, from that split 0.31 releases in individual repositories.

Suggested path: Core 'virtual' domain `plugwise_bv` (or similar) and retaining `plugwise` as the main integration and introduce `plugwise_usb` (or something similar)
[TODO]: Mitigate coverage decrease
### Todo

Check util.py coverage after 0.30 / #287
[TODO]: Mitigate coverage decrease
### Todo

Check util.py coverage after 0.30 / #287


Discussion / hints:
TODO: Look up coverage of `version_to_model` and why it isn't covered now (+add the HW_MODELS to that test)
## [Codecov](https://codecov.io/gh/plugwise/python-plugwise/pull/287?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=plugwise) Report
> Merging [#287](https://codecov.io/gh/plugwise/python-plugwise/pull/287?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=plugwise) (9414ecd) into [main](https://codecov.io/gh/plugwise/python-plugwise/commit/b912cc93e8b17de469cf0215d78d12faf554f54f?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=plugwise) (b912cc9) will **decrease** coverage by `0.04%`.
> The diff coverage is `98.41%`.

```diff
@@             Coverage Diff             @@
##              main     #287      +/-   ##
===========================================
- Coverage   100.00%   99.96%   -0.04%     
===========================================
  Files            6        8       +2     
  Lines         2680     2633      -47     
===========================================
- Hits          2680     2632      -48     
- Misses           0        1       +1     
```


| [Impacted Files](https://codecov.io/gh/plugwise/python-plugwise/pull/287?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=plugwise) | Coverage Δ | |
|---|---|---|
| [plugwise/smile/\_\_init\_\_.py](https://codecov.io/gh/plugwise/python-plugwise/pull/287?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=plugwise#diff-cGx1Z3dpc2Uvc21pbGUvX19pbml0X18ucHk=) | `100.00% <ø> (ø)` | |
| [plugwise/smile/exceptions.py](https://codecov.io/gh/plugwise/python-plugwise/pull/287?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=plugwise#diff-cGx1Z3dpc2Uvc21pbGUvZXhjZXB0aW9ucy5weQ==) | `100.00% <ø> (ø)` | |
| [plugwise/smile/helper.py](https://codecov.io/gh/plugwise/python-plugwise/pull/287?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=plugwise#diff-cGx1Z3dpc2Uvc21pbGUvaGVscGVyLnB5) | `100.00% <ø> (ø)` | |
| [plugwise/smile/util.py](https://codecov.io/gh/plugwise/python-plugwise/pull/287?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=plugwise#diff-cGx1Z3dpc2Uvc21pbGUvdXRpbC5weQ==) | `98.11% <98.11%> (ø)` | |
| [plugwise/smile/constants.py](https://codecov.io/gh/plugwise/python-plugwise/pull/287?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=plugwise#diff-cGx1Z3dpc2Uvc21pbGUvY29uc3RhbnRzLnB5) | `100.00% <100.00%> (ø)` | |
| [tests/test\_smile.py](https://codecov.io/gh/plugwise/python-plugwise/pull/287?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=plugwise#diff-dGVzdHMvdGVzdF9zbWlsZS5weQ==) | `100.00% <100.00%> (ø)` | |
| [tests/test\_stick.py](https://codecov.io/gh/plugwise/python-plugwise/pull/287?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=plugwise#diff-dGVzdHMvdGVzdF9zdGljay5weQ==) | `100.00% <100.00%> (ø)` | |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=plugwise)

https://github.com/plugwise/python-plugwise-usb/pull/1 seems promising 

Still bugged by the codecov reduce
Suggesting to confirm if we want to split - follow-up PR should become v0.31 initially and move `plugwise/smile` back to `plugwise/` (while removing USB/Usb-specific-Stick).

Also suggesting to move the codecov `util.py` towards follow-up PR
Awaiting feedback from @brefra (via this PR or PlugwiseDevs Discord)
Merging as discussed



Fix patch:
diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml
index b1e180a94..ad148c760 100644
--- a/.github/ISSUE_TEMPLATE/bug.yml
+++ b/.github/ISSUE_TEMPLATE/bug.yml
@@ -81,13 +81,12 @@ body:
     attributes:
       label: What kind of Plugwise device is having issues?
       description: >
-        Select the best possible option (i.e. for issues with a Lisa or Tom, select Adam - for issues with a Circle, select Stretch or USB).
+        Select the best possible option (i.e. for issues with a Lisa or Tom, select Adam, etc)
       options:
         - "Smile: Adam (including Lisa, Tom, Floor)"
         - "Smile: Anna"
         - "Smile: P1"
         - "Smile: Stretch"
-        - "USB: Stick"
         - "Other product (we might not (yet) know of it's existence!)"
   - type: input
     validations:
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
index d319c47ee..7bb26a902 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -83,13 +83,12 @@ body:
     attributes:
       label: What kind of Plugwise device is having issues?
       description: >
-        Select the best possible option (i.e. for issues with a Lisa or Tom, select Adam - for issues with a Circle, select Stretch or USB).
+        Select the best possible option (i.e. for issues with a Lisa or Tom, select Adam, etc.)
       options:
         - "Smile: Adam (including Lisa, Tom, Floor)"
         - "Smile: Anna"
         - "Smile: P1"
         - "Smile: Stretch"
-        - "USB: Stick"
         - "Other product (we might not (yet) know of it's existence!)"
   - type: input
     validations:
diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml
index 3485eb028..32cf1c0be 100644
--- a/.github/workflows/verify.yml
+++ b/.github/workflows/verify.yml
@@ -4,7 +4,7 @@
 name: Latest commit
 
 env:
-  CACHE_VERSION: 9
+  CACHE_VERSION: 10
   DEFAULT_PYTHON: "3.11"
   PRE_COMMIT_HOME: ~/.cache/pre-commit
 
@@ -113,7 +113,10 @@ jobs:
   commitcheck:
     runs-on: ubuntu-latest
     name: Check commit
-    needs: black
+    needs:
+      - black
+      - shellcheck
+      - dependencies_check
     steps:
       - name: Check out committed code
         uses: actions/checkout@v3
@@ -399,4 +402,4 @@ jobs:
         run: |
           . venv/bin/activate
           echo "Showing complexity higher or equal to 'C'"
-          radon cc plugwise/smile.py plugwise/helper.py tests/test_smile.py -s -nc --no-assert
+          radon cc plugwise/ tests/ -s -nc --no-assert
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1ec96febe..ba519d77b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,12 @@
 # Changelog
 
-## Ongoing: Logic separation between USB and non-USB
+## v0.31 refactoring (alpha only, effectively not to be released): Logic separation between USB and non-USB
+
+- Smile component (suggested `python-plugwise-usb` for USB (or stick-standalone) route)
+
+## v0.30 refactoring (alpha only, effectively unreleased)
+
+- More formal Split between Networked and USB code
 
 ## v0.27.10: Anna + Elga: final fix for [#320](https://github.com/plugwise/plugwise-beta/issues/320)
 
diff --git a/CODEOWNERS b/CODEOWNERS
index 673c87e1b..976c00d2d 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -15,15 +15,5 @@ requirements*.txt @plugwise/devs
 /tests/ @bouwew @compatech
 /scripts/ @bouwew @compatech
 
-# NET specific
-/plugwise/smile.py @bouwew @compatech
-
-# USB specific
-/plugwise/connections/ @brefra
-/plugwise/controller.py @bouwew @brefra
-/plugwise/messages/ @brefra
-/plugwise/nodes/ @brefra
-/plugwise/parser.py @brefra
-/plugwise/stick.py @brefra
-/plugwise/util.py @brefra
-
+# Networked Plugwise components
+/plugwise @bouwew @compatech
diff --git a/README.md b/README.md
index 800a77fd2..b3c8864f9 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,8 @@
 # Plugwise python module
 
-This module is the backend for the [`plugwise` component](https://github.com/home-assistant/core/tree/dev/homeassistant/components/plugwise) which we maintain as co-code owners Home Assistant Core.
+This module is the backend for the [`plugwise` component](https://github.com/home-assistant/core/tree/dev/homeassistant/components/plugwise) in Home Assistant Core (which we maintain as co-code owners).
+
+This module supports `Smile`s (and `Stretch`), i.e. the networked plugwise devices. For the USB (or Stick-standalone version) please refer to upcoming [`plugwise-usb` component](https://github.com/plugwise/plugwise-usb-beta).
 
 Our main usage for this module is supporting [Home Assistant](https://www.home-assistant.io) / [home-assistant](http://github.com/home-assistant/core/)
 
@@ -67,15 +69,6 @@ Module providing interfacing with the Plugwise devices:
 - [x] Stretch
 - [ ] Some of the equipment mentioned in USB when in use via Stretch or Adam
 
-### USB
-
-- [x] Stick
-  - [x] Circle+ / Stealth+
-  - [x] Circle / Stealth
-  - [x] Scan
-  The devices listed below have **NOT** been tested and are therefore unknown for their correct operation
-    - [x] Sense
-    - [x] Switch
 - [x] [Home-Assistant](https://home-assistant.io) via
   - [x] Native supporting networked Plugwise products
   - [x] [HACS](https://hacs.xyz) and `custom_component` [Plugwise Beta](https://github.com/plugwise/plugwise-beta/) (supporting all devices above)
@@ -86,6 +79,7 @@ As per the origins we have retained the appropriate licensing and including the
 
 Origins (from newest to oldest):
 
+- Networked (Smile/Stretch) Plugwise support by @bouwew (Bouwe) and @CoMPaTech (Tom). We both support and help out @brefra (Frank) where possible, he's supporting the USB module and integration.
 - 'All' available Plugwise support by @bouwew (Bouwe), @brefra (Frank) and @CoMPaTech (Tom)
 - Upstreamed haanna/HA-core Anna, including all later products - 'Plugwise-Smile/Plugwise-HA/plugwise-beta` by @bouwew (Bouwe) & @CoMPaTech (Tom)
 - Networked Plugwise Anna module with custom_module - `haanna/anna-ha` via <https://github.com/laetificat> (Kevin)
diff --git a/plugwise/DEPENDENCIES.md b/plugwise/DEPENDENCIES.md
deleted file mode 100644
index 557d01a7a..000000000
--- a/plugwise/DEPENDENCIES.md
+++ /dev/null
@@ -1,16 +0,0 @@
-# Cross domain functionalities
-
-Below contents are the dependencies between the various supported Plugwise domains.
-
-## Smile / Stretch
-
-- Manual copy in of HW_MODELS out of constants
-- Duplicated `util.py` functionality for `version_to_model`
-
-## Smile generic and legacy
-
-- No cross dependencies, lots of dependencies
-
-## Stick
-
-- Duplicated `util.py` functionality for `version_to_model`
diff --git a/plugwise/__init__.py b/plugwise/__init__.py
index 695f9fbd8..0248730bc 100644
--- a/plugwise/__init__.py
+++ b/plugwise/__init__.py
@@ -1,4 +1,821 @@
-"""Plugwise module."""
+"""Use of this source code is governed by the MIT license found in the LICENSE file.
 
-from plugwise.smile import Smile  # noqa: F401
-from plugwise.stick import Stick  # noqa: F401
+Plugwise backend module for Home Assistant Core.
+"""
+from __future__ import annotations
+
+import aiohttp
+from defusedxml import ElementTree as etree
+
+# Dict as class
+from munch import Munch
+
+# Version detection
+import semver
+
+from .constants import (
+    APPLIANCES,
+    DEFAULT_PORT,
+    DEFAULT_TIMEOUT,
+    DEFAULT_USERNAME,
+    DOMAIN_OBJECTS,
+    LEGACY_SMILES,
+    LOCATIONS,
+    LOGGER,
+    MAX_SETPOINT,
+    MIN_SETPOINT,
+    MODULES,
+    NOTIFICATIONS,
+    RULES,
+    SMILES,
+    STATUS,
+    SWITCH_GROUP_TYPES,
+    SYSTEM,
+    ZONE_THERMOSTATS,
+    ActuatorData,
+    ApplianceData,
+    DeviceData,
+    GatewayData,
+    SmileBinarySensors,
+    SmileSensors,
+    SmileSwitches,
+)
+from .exceptions import (
+    InvalidSetupError,
+    PlugwiseError,
+    ResponseError,
+    UnsupportedDeviceError,
+)
+from .helper import SmileComm, SmileHelper, update_helper
+
+
+class SmileData(SmileHelper):
+    """The Plugwise Smile main class."""
+
+    def update_for_cooling(self, device: DeviceData) -> None:
+        """Helper-function for adding/updating various cooling-related values."""
+        # Add setpoint_low and setpoint_high when cooling is enabled
+        if device["dev_class"] not in ZONE_THERMOSTATS:
+            return
+
+        # For heating + cooling, replace setpoint with setpoint_high/_low
+        if self._cooling_present:
+            thermostat = device["thermostat"]
+            sensors = device["sensors"]
+            max_setpoint = MAX_SETPOINT
+            min_setpoint = MIN_SETPOINT
+            if device["selected_schedule"] != "None":
+                max_setpoint = self._sched_setpoints[1]
+                min_setpoint = self._sched_setpoints[0]
+
+            temp_dict: ActuatorData = {
+                "setpoint_low": thermostat["setpoint"],
+                "setpoint_high": max_setpoint,
+            }
+            if self._cooling_enabled:
+                temp_dict = {
+                    "setpoint_low": min_setpoint,
+                    "setpoint_high": thermostat["setpoint"],
+                }
+            thermostat.pop("setpoint")
+            temp_dict.update(thermostat)
+            device["thermostat"] = temp_dict
+            if "setpoint" in sensors:
+                sensors.pop("setpoint")
+            sensors["setpoint_low"] = temp_dict["setpoint_low"]
+            sensors["setpoint_high"] = temp_dict["setpoint_high"]
+
+    def _all_device_data(self) -> None:
+        """Helper-function for get_all_devices().
+
+        Collect initial data for each device and add to self.gw_data and self.gw_devices.
+        """
+        for device_id, device in self._appl_data.items():
+            bs_dict: SmileBinarySensors = {}
+            s_dict: SmileSensors = {}
+            sw_dict: SmileSwitches = {}
+            data = self._get_device_data(device_id)
+            self.gw_devices[device_id] = self._update_device_with_dicts(
+                device_id, data, device, bs_dict, s_dict, sw_dict
+            )
+
+            # Update for cooling
+            self.update_for_cooling(self.gw_devices[device_id])
+
+        self.gw_data.update(
+            {"smile_name": self.smile_name, "gateway_id": self.gateway_id}
+        )
+        if self._is_thermostat:
+            self.gw_data.update(
+                {"heater_id": self._heater_id, "cooling_present": self._cooling_present}
+            )
+
+    def get_all_devices(self) -> None:
+        """Determine the evices present from the obtained XML-data.
+
+        Run this functions once to gather the initial device configuration,
+        then regularly run async_update() to refresh the device data.
+        """
+        # Start by determining the system capabilities:
+        # Find the connected heating/cooling device (heater_central), e.g. heat-pump or gas-fired heater
+        if self.smile_type == "thermostat":
+            onoff_boiler: etree = self._domain_objects.find(
+                "./module/protocols/onoff_boiler"
+            )
+            open_therm_boiler: etree = self._domain_objects.find(
+                "./module/protocols/open_therm_boiler"
+            )
+            self._on_off_device = onoff_boiler is not None
+            self._opentherm_device = open_therm_boiler is not None
+
+            # Determine the presence of special features
+            locator_1 = "./gateway/features/cooling"
+            locator_2 = "./gateway/features/elga_support"
+            search = self._domain_objects
+            if search.find(locator_1) is not None:
+                self._cooling_present = True
+            if search.find(locator_2) is not None:
+                self._elga = True
+
+        # Gather all the device and initial data
+        self._scan_thermostats()
+
+        if group_data := self._group_switches():
+            self._appl_data.update(group_data)
+
+        # Collect data for each device via helper function
+        self._all_device_data()
+
+    def _device_data_switching_group(
+        self, details: ApplianceData, device_data: DeviceData
+    ) -> DeviceData:
+        """Helper-function for _get_device_data().
+
+        Determine switching group device data.
+        """
+        if details["dev_class"] in SWITCH_GROUP_TYPES:
+            counter = 0
+            for member in details["members"]:
+                member_data = self._get_appliance_data(member)
+                if member_data.get("relay"):
+                    counter += 1
+
+            device_data["relay"] = counter != 0
+
+        return device_data
+
+    def _device_data_adam(
+        self, details: ApplianceData, device_data: DeviceData
+    ) -> DeviceData:
+        """Helper-function for _get_device_data().
+
+        Determine Adam device data.
+        """
+        # Indicate heating_state based on valves being open in case of city-provided heating
+        if (
+            self.smile_name == "Adam"
+            and details.get("dev_class") == "heater_central"
+            and self._on_off_device
+            and self._heating_valves() is not None
+        ):
+            device_data["heating_state"] = self._heating_valves() != 0
+
+        return device_data
+
+    def _device_data_climate(
+        self, details: ApplianceData, device_data: DeviceData
+    ) -> DeviceData:
+        """Helper-function for _get_device_data().
+
+        Determine climate-control device data.
+        """
+        loc_id = details["location"]
+
+        # Presets
+        device_data["preset_modes"] = None
+        device_data["active_preset"] = None
+        if presets := self._presets(loc_id):
+            presets_list = list(presets)
+            device_data["preset_modes"] = presets_list
+            device_data["active_preset"] = self._preset(loc_id)
+
+        # Schedule
+        (
+            avail_schedules,
+            sel_schedule,
+            self._sched_setpoints,
+            last_active,
+        ) = self._schedules(loc_id)
+        device_data["available_schedules"] = avail_schedules
+        device_data["selected_schedule"] = sel_schedule
+        if self._smile_legacy:
+            device_data["last_used"] = "".join(map(str, avail_schedules))
+        else:
+            device_data["last_used"] = last_active
+
+        # Control_state, only for Adam master thermostats
+        if ctrl_state := self._control_state(loc_id):
+            device_data["control_state"] = ctrl_state
+
+        # Operation modes: auto, heat, heat_cool
+        device_data["mode"] = "auto"
+        if sel_schedule == "None":
+            device_data["mode"] = "heat"
+            if self._cooling_present:
+                device_data["mode"] = "heat_cool"
+
+        if "None" not in avail_schedules:
+            loc_schedule_states = {}
+            for schedule in avail_schedules:
+                loc_schedule_states[schedule] = "off"
+                if device_data["mode"] == "auto":
+                    loc_schedule_states[sel_schedule] = "on"
+
+            self._schedule_old_states[loc_id] = loc_schedule_states
+
+        return device_data
+
+    def _check_availability(
+        self, details: ApplianceData, device_data: DeviceData
+    ) -> None:
+        """Helper-function for _get_device_data().
+
+        Provide availability status for the wired-commected devices.
+        """
+        # OpenTherm device
+        if details["dev_class"] == "heater_central" and details["name"] != "OnOff":
+            device_data["available"] = True
+            for data in self._notifications.values():
+                for msg in data.values():
+                    if "no OpenTherm communication" in msg:
+                        device_data["available"] = False
+
+        # Smartmeter
+        if details["dev_class"] == "smartmeter":
+            device_data["available"] = True
+            for data in self._notifications.values():
+                for msg in data.values():
+                    if "P1 does not seem to be connected to a smart meter" in msg:
+                        device_data["available"] = False
+
+    def _get_device_data(self, dev_id: str) -> DeviceData:
+        """Helper-function for _all_device_data() and async_update().
+
+        Provide device-data, based on Location ID (= dev_id), from APPLIANCES.
+        """
+        details = self._appl_data[dev_id]
+        device_data = self._get_appliance_data(dev_id)
+        # Remove thermostat-dict for thermo_sensors
+        if details["dev_class"] == "thermo_sensor":
+            device_data.pop("thermostat")
+
+        # Generic
+        if self.smile_type == "thermostat" and details["dev_class"] == "gateway":
+            # Adam & Anna: the Smile outdoor_temperature is present in DOMAIN_OBJECTS and LOCATIONS - under Home
+            # The outdoor_temperature present in APPLIANCES is a local sensor connected to the active device
+            outdoor_temperature = self._object_value(
+                self._home_location, "outdoor_temperature"
+            )
+            if outdoor_temperature is not None:
+                device_data["outdoor_temperature"] = outdoor_temperature
+
+            # Show the allowed regulation modes
+            if self._reg_allowed_modes:
+                device_data["regulation_modes"] = self._reg_allowed_modes
+
+        # Show the allowed dhw_modes
+        if details["dev_class"] == "heater_central" and self._dhw_allowed_modes:
+            device_data["dhw_modes"] = self._dhw_allowed_modes
+
+        # Get P1 smartmeter data from LOCATIONS or MODULES
+        if details["dev_class"] == "smartmeter":
+            if not self._smile_legacy:
+                device_data.update(self._power_data_from_location(details["location"]))
+            else:
+                device_data.update(self._power_data_from_modules())
+
+        # Check availability of non-legacy wired-connected devices
+        if not self._smile_legacy:
+            self._check_availability(details, device_data)
+
+        # Switching groups data
+        device_data = self._device_data_switching_group(details, device_data)
+        # Specific, not generic Adam data
+        device_data = self._device_data_adam(details, device_data)
+        # No need to obtain thermostat data when the device is not a thermostat
+        if details["dev_class"] not in ZONE_THERMOSTATS:
+            return device_data
+
+        # Thermostat data (presets, temperatures etc)
+        device_data = self._device_data_climate(details, device_data)
+
+        return device_data
+
+
+class Smile(SmileComm, SmileData):
+    """The Plugwise SmileConnect class."""
+
+    # pylint: disable=too-many-instance-attributes, too-many-public-methods
+
+    def __init__(
+        self,
+        host: str,
+        password: str,
+        username: str = DEFAULT_USERNAME,
+        port: int = DEFAULT_PORT,
+        timeout: float = DEFAULT_TIMEOUT,
+        websession: aiohttp.ClientSession | None = None,
+    ) -> None:
+        """Set the constructor for this class."""
+        super().__init__(
+            host,
+            password,
+            username,
+            port,
+            timeout,
+            websession,
+        )
+        SmileData.__init__(self)
+
+        self.smile_hostname: str | None = None
+
+    async def connect(self) -> bool:
+        """Connect to Plugwise device and determine its name, type and version."""
+        result = await self._request(DOMAIN_OBJECTS)
+        # Work-around for Stretch fv 2.7.18
+        if not (vendor_names := result.findall("./module/vendor_name")):
+            result = await self._request(MODULES)
+            vendor_names = result.findall("./module/vendor_name")
+
+        names: list[str] = []
+        for name in vendor_names:
+            names.append(name.text)
+
+        vendor_models = result.findall("./module/vendor_model")
+        models: list[str] = []
+        for model in vendor_models:
+            models.append(model.text)
+
+        dsmrmain = result.find("./module/protocols/dsmrmain")
+        if "Plugwise" not in names and dsmrmain is None:  # pragma: no cover
+            LOGGER.error(
+                "Connected but expected text not returned, we got %s. Please create \
+                an issue on http://github.com/plugwise/python-plugwise",
+                result,
+            )
+            raise ResponseError
+
+        # Check if Anna is connected to an Adam
+        if "159.2" in models:
+            LOGGER.error(
+                "Your Anna is connected to an Adam, make sure to only add the Adam as integration."
+            )
+            raise InvalidSetupError
+
+        # Determine smile specifics
+        await self._smile_detect(result, dsmrmain)
+
+        # Update all endpoints on first connect
+        await self._full_update_device()
+
+        return True
+
+    async def _smile_detect_legacy(self, result: etree, dsmrmain: etree) -> str:
+        """Helper-function for _smile_detect()."""
+        # Stretch: find the MAC of the zigbee master_controller (= Stick)
+        if network := result.find("./module/protocols/master_controller"):
+            self.smile_zigbee_mac_address = network.find("mac_address").text
+        # Find the active MAC in case there is an orphaned Stick
+        if zb_networks := result.findall("./network"):
+            for zb_network in zb_networks:
+                if zb_network.find("./nodes/network_router"):
+                    network = zb_network.find("./master_controller")
+                    self.smile_zigbee_mac_address = network.find("mac_address").text
+
+        # Assume legacy
+        self._smile_legacy = True
+        # Try if it is a legacy Anna, assuming appliance thermostat,
+        # fake insert version assuming Anna, couldn't find another way to identify as legacy Anna
+        self.smile_fw_version = "1.8.0"
+        model = "smile_thermo"
+        if result.find('./appliance[type="thermostat"]') is None:
+            # It's a P1 legacy:
+            if dsmrmain is not None:
+                self._status = await self._request(STATUS)
+                self.smile_fw_version = self._status.find("./system/version").text
+                model = self._status.find("./system/product").text
+                self.smile_hostname = self._status.find("./network/hostname").text
+                self.smile_mac_address = self._status.find("./network/mac_address").text
+
+            # Or a legacy Stretch:
+            elif network is not None:
+                self._system = await self._request(SYSTEM)
+                self.smile_fw_version = self._system.find("./gateway/firmware").text
+                model = self._system.find("./gateway/product").text
+                self.smile_hostname = self._system.find("./gateway/hostname").text
+                # If wlan0 contains data it's active, so eth0 should be checked last
+                for network in ("wlan0", "eth0"):
+                    locator = f"./{network}/mac"
+                    if (net_locator := self._system.find(locator)) is not None:
+                        self.smile_mac_address = net_locator.text
+
+            else:  # pragma: no cover
+                # No cornercase, just end of the line
+                LOGGER.error(
+                    "Connected but no gateway device information found, please create \
+                     an issue on http://github.com/plugwise/python-plugwise"
+                )
+                raise ResponseError
+
+        return model
+
+    async def _smile_detect(self, result: etree, dsmrmain: etree) -> None:
+        """Helper-function for connect().
+
+        Detect which type of Smile is connected.
+        """
+        model: str | None = None
+        if (gateway := result.find("./gateway")) is not None:
+            model = gateway.find("vendor_model").text
+            self.smile_fw_version = gateway.find("firmware_version").text
+            self.smile_hw_version = gateway.find("hardware_version").text
+            self.smile_hostname = gateway.find("hostname").text
+            self.smile_mac_address = gateway.find("mac_address").text
+        else:
+            model = await self._smile_detect_legacy(result, dsmrmain)
+
+        if model is None or self.smile_fw_version is None:  # pragma: no cover
+            # Corner case check
+            LOGGER.error(
+                "Unable to find model or version information, please create \
+                 an issue on http://github.com/plugwise/python-plugwise"
+            )
+            raise UnsupportedDeviceError
+
+        ver = semver.VersionInfo.parse(self.smile_fw_version)
+        target_smile = f"{model}_v{ver.major}"
+        LOGGER.debug("Plugwise identified as %s", target_smile)
+        if target_smile not in SMILES:
+            LOGGER.error(
+                'Your version Smile identified as "%s" seems unsupported by our plugin, please \
+                create an issue on http://github.com/plugwise/python-plugwise',
+                target_smile,
+            )
+            raise UnsupportedDeviceError
+
+        self.smile_model = "Gateway"
+        self.smile_name = SMILES[target_smile].smile_name
+        self.smile_type = SMILES[target_smile].smile_type
+        self.smile_version = (self.smile_fw_version, ver)
+
+        if target_smile in LEGACY_SMILES:
+            self._smile_legacy = True
+
+        if self.smile_type == "stretch":
+            self._stretch_v2 = self.smile_version[1].major == 2
+            self._stretch_v3 = self.smile_version[1].major == 3
+
+        self._is_thermostat = self.smile_type == "thermostat"
+
+    async def _full_update_device(self) -> None:
+        """Perform a first fetch of all XML data, needed for initialization."""
+        self._locations = await self._request(LOCATIONS)
+        self._modules = await self._request(MODULES)
+
+        # P1 legacy has no appliances and nothing of interest in domain_objects
+        if not (self.smile_type == "power" and self._smile_legacy):
+            self._appliances = await self._request(APPLIANCES)
+            await self._update_domain_objects()
+
+    async def _update_domain_objects(self) -> None:
+        """Helper-function for smile.py: full_update_device() and async_update().
+
+        Request domain_objects data.
+        """
+        self._domain_objects = await self._request(DOMAIN_OBJECTS)
+
+        # If Plugwise notifications present:
+        self._notifications = {}
+        for notification in self._domain_objects.findall("./notification"):
+            try:
+                msg_id = notification.attrib["id"]
+                msg_type = notification.find("type").text
+                msg = notification.find("message").text
+                self._notifications.update({msg_id: {msg_type: msg}})
+                LOGGER.debug("Plugwise notifications: %s", self._notifications)
+            except AttributeError:  # pragma: no cover
+                LOGGER.debug(
+                    "Plugwise notification present but unable to process, manually investigate: %s",
+                    f"{self._endpoint}{DOMAIN_OBJECTS}",
+                )
+
+    async def async_update(self) -> tuple[GatewayData, dict[str, DeviceData]]:
+        """Perform an incremental update for updating the various device states."""
+        if self.smile_type != "power":
+            await self._update_domain_objects()
+        elif not self._smile_legacy:
+            self._locations = await self._request(LOCATIONS)
+        else:
+            self._modules = await self._request(MODULES)
+
+        # P1 legacy has no appliances
+        if not (self.smile_type == "power" and self._smile_legacy):
+            self._appliances = await self._request(APPLIANCES)
+
+        self.gw_data["notifications"] = self._notifications
+
+        for dev_id, dev_dict in self.gw_devices.items():
+            data = self._get_device_data(dev_id)
+            for key, value in data.items():
+                if key in dev_dict:
+                    dev_dict[key] = value  # type: ignore [literal-required]
+
+            for item in ("binary_sensors", "sensors", "switches"):
+                notifs: dict[str, dict[str, str]] = {}
+                if item == "binary_sensors":
+                    notifs = self._notifications
+                if item in dev_dict:
+                    for key in data:
+                        update_helper(
+                            data,
+                            self.gw_devices,
+                            dev_dict,
+                            dev_id,
+                            item,
+                            key,
+                            notifs,
+                        )
+
+            # Update for cooling
+            self.update_for_cooling(dev_dict)
+
+        return (self.gw_data, self.gw_devices)
+
+    async def _set_schedule_state_legacy(
+        self, loc_id: str, name: str, status: str
+    ) -> None:
+        """Helper-function for set_schedule_state()."""
+        schedule_rule_id: str | None = None
+        for rule in self._domain_objects.findall("rule"):
+            if rule.find("name").text == name:
+                schedule_rule_id = rule.attrib["id"]
+
+        if schedule_rule_id is None:
+            raise PlugwiseError("Plugwise: no schedule with this name available.")
+
+        new_state = "false"
+        if status == "on":
+            new_state = "true"
+        # If no state change is requested, do nothing
+        if new_state == self._schedule_old_states[loc_id][name]:
+            return
+
+        locator = f'.//*[@id="{schedule_rule_id}"]/template'
+        for rule in self._domain_objects.findall(locator):
+            template_id = rule.attrib["id"]
+
+        uri = f"{RULES};id={schedule_rule_id}"
+        data = (
+            "<rules><rule"
+            f' id="{schedule_rule_id}"><name><![CDATA[{name}]]></name><template'
+            f' id="{template_id}" /><active>{new_state}</active></rule></rules>'
+        )
+
+        await self._request(uri, method="put", data=data)
+        self._schedule_old_states[loc_id][name] = new_state
+
+    async def set_schedule_state(
+        self, loc_id: str, name: str | None, new_state: str
+    ) -> None:
+        """Activate/deactivate the Schedule, with the given name, on the relevant Thermostat.
+
+        Determined from - DOMAIN_OBJECTS.
+        In HA Core used to set the hvac_mode: in practice switch between schedule on - off.
+        """
+        # Input checking
+        if new_state not in ["on", "off"]:
+            raise PlugwiseError("Plugwise: invalid schedule state.")
+        if name is None:
+            raise PlugwiseError(
+                "Plugwise: cannot change schedule-state: no schedule name provided"
+            )
+
+        if self._smile_legacy:
+            await self._set_schedule_state_legacy(loc_id, name, new_state)
+            return
+
+        schedule_rule = self._rule_ids_by_name(name, loc_id)
+        # Raise an error when the schedule name does not exist
+        if not schedule_rule or schedule_rule is None:
+            raise PlugwiseError("Plugwise: no schedule with this name available.")
+
+        # If no state change is requested, do nothing
+        if new_state == self._schedule_old_states[loc_id][name]:
+            return
+
+        schedule_rule_id: str = next(iter(schedule_rule))
+
+        template = (
+            '<template tag="zone_preset_based_on_time_and_presence_with_override" />'
+        )
+        if self.smile_name != "Adam":
+            locator = f'.//*[@id="{schedule_rule_id}"]/template'
+            template_id = self._domain_objects.find(locator).attrib["id"]
+            template = f'<template id="{template_id}" />'
+
+        locator = f'.//*[@id="{schedule_rule_id}"]/contexts'
+        contexts = self._domain_objects.find(locator)
+        locator = f'.//*[@id="{loc_id}"].../...'
+        if (subject := contexts.find(locator)) is None:
+            subject = f'<context><zone><location id="{loc_id}" /></zone></context>'
+            subject = etree.fromstring(subject)
+
+        if new_state == "off":
+            self._last_active[loc_id] = name
+            contexts.remove(subject)
+        if new_state == "on":
+            contexts.append(subject)
+
+        contexts = etree.tostring(contexts, encoding="unicode").rstrip()
+
+        uri = f"{RULES};id={schedule_rule_id}"
+        data = (
+            f'<rules><rule id="{schedule_rule_id}"><name><![CDATA[{name}]]></name>'
+            f"{template}{contexts}</rule></rules>"
+        )
+        await self._request(uri, method="put", data=data)
+        self._schedule_old_states[loc_id][name] = new_state
+
+    async def _set_preset_legacy(self, preset: str) -> None:
+        """Set the given Preset on the relevant Thermostat - from DOMAIN_OBJECTS."""
+        locator = f'rule/directives/when/then[@icon="{preset}"].../.../...'
+        rule = self._domain_objects.find(locator)
+        data = f'<rules><rule id="{rule.attrib["id"]}"><active>true</active></rule></rules>'
+
+        await self._request(RULES, method="put", data=data)
+
+    async def set_preset(self, loc_id: str, preset: str) -> None:
+        """Set the given Preset on the relevant Thermostat - from LOCATIONS."""
+        if (presets := self._presets(loc_id)) is None:
+            raise PlugwiseError("Plugwise: no presets available.")  # pragma: no cover
+        if preset not in list(presets):
+            raise PlugwiseError("Plugwise: invalid preset.")
+
+        if self._smile_legacy:
+            await self._set_preset_legacy(preset)
+            return
+
+        current_location = self._locations.find(f'location[@id="{loc_id}"]')
+        location_name = current_location.find("name").text
+        location_type = current_location.find("type").text
+
+        uri = f"{LOCATIONS};id={loc_id}"
+        data = (
+            "<locations><location"
+            f' id="{loc_id}"><name>{location_name}</name><type>{location_type}'
+            f"</type><preset>{preset}</preset></location></locations>"
+        )
+
+        await self._request(uri, method="put", data=data)
+
+    async def set_temperature(self, loc_id: str, items: dict[str, float]) -> None:
+        """Set the given Temperature on the relevant Thermostat."""
+        setpoint: float | None = None
+        if "setpoint" in items:
+            setpoint = items["setpoint"]
+        if self._cooling_present:
+            if "setpoint_low" in items:
+                setpoint = items["setpoint_low"]
+            if self._cooling_active:
+                if "setpoint_high" in items:
+                    setpoint = items["setpoint_high"]
+
+        if setpoint is None:
+            raise PlugwiseError(
+                "Plugwise: failed setting temperature: no valid input provided"
+            )  # pragma: no cover"
+
+        temperature = str(setpoint)
+        uri = self._thermostat_uri(loc_id)
+        data = (
+            "<thermostat_functionality><setpoint>"
+            f"{temperature}</setpoint></thermostat_functionality>"
+        )
+
+        await self._request(uri, method="put", data=data)
+
+    async def set_number_setpoint(self, key: str, temperature: float) -> None:
+        """Set the max. Boiler or DHW setpoint on the Central Heating boiler."""
+        temp = str(temperature)
+        thermostat_id: str | None = None
+        locator = f'appliance[@id="{self._heater_id}"]/actuator_functionalities/thermostat_functionality'
+        if th_func_list := self._appliances.findall(locator):
+            for th_func in th_func_list:
+                if th_func.find("type").text == key:
+                    thermostat_id = th_func.attrib["id"]
+
+        if thermostat_id is None:
+            raise PlugwiseError(f"Plugwise: cannot change setpoint, {key} not found.")
+
+        uri = f"{APPLIANCES};id={self._heater_id}/thermostat;id={thermostat_id}"
+        data = f"<thermostat_functionality><setpoint>{temp}</setpoint></thermostat_functionality>"
+        await self._request(uri, method="put", data=data)
+
+    async def _set_groupswitch_member_state(
+        self, members: list[str], state: str, switch: Munch
+    ) -> None:
+        """Helper-function for set_switch_state().
+
+        Set the given State of the relevant Switch within a group of members.
+        """
+        for member in members:
+            locator = f'appliance[@id="{member}"]/{switch.actuator}/{switch.func_type}'
+            switch_id = self._appliances.find(locator).attrib["id"]
+            uri = f"{APPLIANCES};id={member}/{switch.device};id={switch_id}"
+            if self._stretch_v2:
+                uri = f"{APPLIANCES};id={member}/{switch.device}"
+            data = f"<{switch.func_type}><{switch.func}>{state}</{switch.func}></{switch.func_type}>"
+
+            await self._request(uri, method="put", data=data)
+
+    async def set_switch_state(
+        self, appl_id: str, members: list[str] | None, model: str, state: str
+    ) -> None:
+        """Set the given State of the relevant Switch."""
+        switch = Munch()
+        switch.actuator = "actuator_functionalities"
+        switch.device = "relay"
+        switch.func_type = "relay_functionality"
+        switch.func = "state"
+        if model == "dhw_cm_switch":
+            switch.device = "toggle"
+            switch.func_type = "toggle_functionality"
+            switch.act_type = "domestic_hot_water_comfort_mode"
+
+        if model == "cooling_ena_switch":
+            switch.device = "toggle"
+            switch.func_type = "toggle_functionality"
+            switch.act_type = "cooling_enabled"
+
+        if model == "lock":
+            switch.func = "lock"
+            state = "false" if state == "off" else "true"
+
+        if self._stretch_v2:
+            switch.actuator = "actuators"
+            switch.func_type = "relay"
+
+        if members is not None:
+            return await self._set_groupswitch_member_state(members, state, switch)
+
+        locator = f'appliance[@id="{appl_id}"]/{switch.actuator}/{switch.func_type}'
+        found: list[etree] = self._appliances.findall(locator)
+        for item in found:
+            if (sw_type := item.find("type")) is not None:
+                if sw_type.text == switch.act_type:
+                    switch_id = item.attrib["id"]
+            else:
+                switch_id = item.attrib["id"]
+                break
+
+        uri = f"{APPLIANCES};id={appl_id}/{switch.device};id={switch_id}"
+        if self._stretch_v2:
+            uri = f"{APPLIANCES};id={appl_id}/{switch.device}"
+        data = f"<{switch.func_type}><{switch.func}>{state}</{switch.func}></{switch.func_type}>"
+
+        if model == "relay":
+            locator = (
+                f'appliance[@id="{appl_id}"]/{switch.actuator}/{switch.func_type}/lock'
+            )
+            # Don't bother switching a relay when the corresponding lock-state is true
+            if self._appliances.find(locator).text == "true":
+                raise PlugwiseError("Plugwise: the locked Relay was not switched.")
+
+        await self._request(uri, method="put", data=data)
+
+    async def set_regulation_mode(self, mode: str) -> None:
+        """Set the heating regulation mode."""
+        if mode not in self._reg_allowed_modes:
+            raise PlugwiseError("Plugwise: invalid regulation mode.")
+
+        uri = f"{APPLIANCES};type=gateway/regulation_mode_control"
+        duration = ""
+        if "bleeding" in mode:
+            duration = "<duration>300</duration>"
+        data = f"<regulation_mode_control_functionality>{duration}<mode>{mode}</mode></regulation_mode_control_functionality>"
+
+        await self._request(uri, method="put", data=data)
+
+    async def set_dhw_mode(self, mode: str) -> None:
+        """Set the domestic hot water heating regulation mode."""
+        if mode not in self._dhw_allowed_modes:
+            raise PlugwiseError("Plugwise: invalid dhw mode.")
+
+        uri = f"{APPLIANCES};type=heater_central/domestic_hot_water_mode_control"
+        data = f"<domestic_hot_water_mode_control_functionality><mode>{mode}</mode></domestic_hot_water_mode_control_functionality>"
+
+        await self._request(uri, method="put", data=data)
+
+    async def delete_notification(self) -> None:
+        """Delete the active Plugwise Notification."""
+        await self._request(NOTIFICATIONS, method="delete")
diff --git a/plugwise/smile/constants.py b/plugwise/constants.py
similarity index 100%
rename from plugwise/smile/constants.py
rename to plugwise/constants.py
diff --git a/plugwise/smile/exceptions.py b/plugwise/exceptions.py
similarity index 100%
rename from plugwise/smile/exceptions.py
rename to plugwise/exceptions.py
diff --git a/plugwise/smile/helper.py b/plugwise/helper.py
similarity index 100%
rename from plugwise/smile/helper.py
rename to plugwise/helper.py
diff --git a/plugwise/smile/__init__.py b/plugwise/smile/__init__.py
deleted file mode 100644
index 0248730bc..000000000
--- a/plugwise/smile/__init__.py
+++ /dev/null
@@ -1,821 +0,0 @@
-"""Use of this source code is governed by the MIT license found in the LICENSE file.
-
-Plugwise backend module for Home Assistant Core.
-"""
-from __future__ import annotations
-
-import aiohttp
-from defusedxml import ElementTree as etree
-
-# Dict as class
-from munch import Munch
-
-# Version detection
-import semver
-
-from .constants import (
-    APPLIANCES,
-    DEFAULT_PORT,
-    DEFAULT_TIMEOUT,
-    DEFAULT_USERNAME,
-    DOMAIN_OBJECTS,
-    LEGACY_SMILES,
-    LOCATIONS,
-    LOGGER,
-    MAX_SETPOINT,
-    MIN_SETPOINT,
-    MODULES,
-    NOTIFICATIONS,
-    RULES,
-    SMILES,
-    STATUS,
-    SWITCH_GROUP_TYPES,
-    SYSTEM,
-    ZONE_THERMOSTATS,
-    ActuatorData,
-    ApplianceData,
-    DeviceData,
-    GatewayData,
-    SmileBinarySensors,
-    SmileSensors,
-    SmileSwitches,
-)
-from .exceptions import (
-    InvalidSetupError,
-    PlugwiseError,
-    ResponseError,
-    UnsupportedDeviceError,
-)
-from .helper import SmileComm, SmileHelper, update_helper
-
-
-class SmileData(SmileHelper):
-    """The Plugwise Smile main class."""
-
-    def update_for_cooling(self, device: DeviceData) -> None:
-        """Helper-function for adding/updating various cooling-related values."""
-        # Add setpoint_low and setpoint_high when cooling is enabled
-        if device["dev_class"] not in ZONE_THERMOSTATS:
-            return
-
-        # For heating + cooling, replace setpoint with setpoint_high/_low
-        if self._cooling_present:
-            thermostat = device["thermostat"]
-            sensors = device["sensors"]
-            max_setpoint = MAX_SETPOINT
-            min_setpoint = MIN_SETPOINT
-            if device["selected_schedule"] != "None":
-                max_setpoint = self._sched_setpoints[1]
-                min_setpoint = self._sched_setpoints[0]
-
-            temp_dict: ActuatorData = {
-                "setpoint_low": thermostat["setpoint"],
-                "setpoint_high": max_setpoint,
-            }
-            if self._cooling_enabled:
-                temp_dict = {
-                    "setpoint_low": min_setpoint,
-                    "setpoint_high": thermostat["setpoint"],
-                }
-            thermostat.pop("setpoint")
-            temp_dict.update(thermostat)
-            device["thermostat"] = temp_dict
-            if "setpoint" in sensors:
-                sensors.pop("setpoint")
-            sensors["setpoint_low"] = temp_dict["setpoint_low"]
-            sensors["setpoint_high"] = temp_dict["setpoint_high"]
-
-    def _all_device_data(self) -> None:
-        """Helper-function for get_all_devices().
-
-        Collect initial data for each device and add to self.gw_data and self.gw_devices.
-        """
-        for device_id, device in self._appl_data.items():
-            bs_dict: SmileBinarySensors = {}
-            s_dict: SmileSensors = {}
-            sw_dict: SmileSwitches = {}
-            data = self._get_device_data(device_id)
-            self.gw_devices[device_id] = self._update_device_with_dicts(
-                device_id, data, device, bs_dict, s_dict, sw_dict
-            )
-
-            # Update for cooling
-            self.update_for_cooling(self.gw_devices[device_id])
-
-        self.gw_data.update(
-            {"smile_name": self.smile_name, "gateway_id": self.gateway_id}
-        )
-        if self._is_thermostat:
-            self.gw_data.update(
-                {"heater_id": self._heater_id, "cooling_present": self._cooling_present}
-            )
-
-    def get_all_devices(self) -> None:
-        """Determine the evices present from the obtained XML-data.
-
-        Run this functions once to gather the initial device configuration,
-        then regularly run async_update() to refresh the device data.
-        """
-        # Start by determining the system capabilities:
-        # Find the connected heating/cooling device (heater_central), e.g. heat-pump or gas-fired heater
-        if self.smile_type == "thermostat":
-            onoff_boiler: etree = self._domain_objects.find(
-                "./module/protocols/onoff_boiler"
-            )
-            open_therm_boiler: etree = self._domain_objects.find(
-                "./module/protocols/open_therm_boiler"
-            )
-            self._on_off_device = onoff_boiler is not None
-            self._opentherm_device = open_therm_boiler is not None
-
-            # Determine the presence of special features
-            locator_1 = "./gateway/features/cooling"
-            locator_2 = "./gateway/features/elga_support"
-            search = self._domain_objects
-            if search.find(locator_1) is not None:
-                self._cooling_present = True
-            if search.find(locator_2) is not None:
-                self._elga = True
-
-        # Gather all the device and initial data
-        self._scan_thermostats()
-
-        if group_data := self._group_switches():
-            self._appl_data.update(group_data)
-
-        # Collect data for each device via helper function
-        self._all_device_data()
-
-    def _device_data_switching_group(
-        self, details: ApplianceData, device_data: DeviceData
-    ) -> DeviceData:
-        """Helper-function for _get_device_data().
-
-        Determine switching group device data.
-        """
-        if details["dev_class"] in SWITCH_GROUP_TYPES:
-            counter = 0
-            for member in details["members"]:
-                member_data = self._get_appliance_data(member)
-                if member_data.get("relay"):
-                    counter += 1
-
-            device_data["relay"] = counter != 0
-
-        return device_data
-
-    def _device_data_adam(
-        self, details: ApplianceData, device_data: DeviceData
-    ) -> DeviceData:
-        """Helper-function for _get_device_data().
-
-        Determine Adam device data.
-        """
-        # Indicate heating_state based on valves being open in case of city-provided heating
-        if (
-            self.smile_name == "Adam"
-            and details.get("dev_class") == "heater_central"
-            and self._on_off_device
-            and self._heating_valves() is not None
-        ):
-            device_data["heating_state"] = self._heating_valves() != 0
-
-        return device_data
-
-    def _device_data_climate(
-        self, details: ApplianceData, device_data: DeviceData
-    ) -> DeviceData:
-        """Helper-function for _get_device_data().
-
-        Determine climate-control device data.
-        """
-        loc_id = details["location"]
-
-        # Presets
-        device_data["preset_modes"] = None
-        device_data["active_preset"] = None
-        if presets := self._presets(loc_id):
-            presets_list = list(presets)
-            device_data["preset_modes"] = presets_list
-            device_data["active_preset"] = self._preset(loc_id)
-
-        # Schedule
-        (
-            avail_schedules,
-            sel_schedule,
-            self._sched_setpoints,
-            last_active,
-        ) = self._schedules(loc_id)
-        device_data["available_schedules"] = avail_schedules
-        device_data["selected_schedule"] = sel_schedule
-        if self._smile_legacy:
-            device_data["last_used"] = "".join(map(str, avail_schedules))
-        else:
-            device_data["last_used"] = last_active
-
-        # Control_state, only for Adam master thermostats
-        if ctrl_state := self._control_state(loc_id):
-            device_data["control_state"] = ctrl_state
-
-        # Operation modes: auto, heat, heat_cool
-        device_data["mode"] = "auto"
-        if sel_schedule == "None":
-            device_data["mode"] = "heat"
-            if self._cooling_present:
-                device_data["mode"] = "heat_cool"
-
-        if "None" not in avail_schedules:
-            loc_schedule_states = {}
-            for schedule in avail_schedules:
-                loc_schedule_states[schedule] = "off"
-                if device_data["mode"] == "auto":
-                    loc_schedule_states[sel_schedule] = "on"
-
-            self._schedule_old_states[loc_id] = loc_schedule_states
-
-        return device_data
-
-    def _check_availability(
-        self, details: ApplianceData, device_data: DeviceData
-    ) -> None:
-        """Helper-function for _get_device_data().
-
-        Provide availability status for the wired-commected devices.
-        """
-        # OpenTherm device
-        if details["dev_class"] == "heater_central" and details["name"] != "OnOff":
-            device_data["available"] = True
-            for data in self._notifications.values():
-                for msg in data.values():
-                    if "no OpenTherm communication" in msg:
-                        device_data["available"] = False
-
-        # Smartmeter
-        if details["dev_class"] == "smartmeter":
-            device_data["available"] = True
-            for data in self._notifications.values():
-                for msg in data.values():
-                    if "P1 does not seem to be connected to a smart meter" in msg:
-                        device_data["available"] = False
-
-    def _get_device_data(self, dev_id: str) -> DeviceData:
-        """Helper-function for _all_device_data() and async_update().
-
-        Provide device-data, based on Location ID (= dev_id), from APPLIANCES.
-        """
-        details = self._appl_data[dev_id]
-        device_data = self._get_appliance_data(dev_id)
-        # Remove thermostat-dict for thermo_sensors
-        if details["dev_class"] == "thermo_sensor":
-            device_data.pop("thermostat")
-
-        # Generic
-        if self.smile_type == "thermostat" and details["dev_class"] == "gateway":
-            # Adam & Anna: the Smile outdoor_temperature is present in DOMAIN_OBJECTS and LOCATIONS - under Home
-            # The outdoor_temperature present in APPLIANCES is a local sensor connected to the active device
-            outdoor_temperature = self._object_value(
-                self._home_location, "outdoor_temperature"
-            )
-            if outdoor_temperature is not None:
-                device_data["outdoor_temperature"] = outdoor_temperature
-
-            # Show the allowed regulation modes
-            if self._reg_allowed_modes:
-                device_data["regulation_modes"] = self._reg_allowed_modes
-
-        # Show the allowed dhw_modes
-        if details["dev_class"] == "heater_central" and self._dhw_allowed_modes:
-            device_data["dhw_modes"] = self._dhw_allowed_modes
-
-        # Get P1 smartmeter data from LOCATIONS or MODULES
-        if details["dev_class"] == "smartmeter":
-            if not self._smile_legacy:
-                device_data.update(self._power_data_from_location(details["location"]))
-            else:
-                device_data.update(self._power_data_from_modules())
-
-        # Check availability of non-legacy wired-connected devices
-        if not self._smile_legacy:
-            self._check_availability(details, device_data)
-
-        # Switching groups data
-        device_data = self._device_data_switching_group(details, device_data)
-        # Specific, not generic Adam data
-        device_data = self._device_data_adam(details, device_data)
-        # No need to obtain thermostat data when the device is not a thermostat
-        if details["dev_class"] not in ZONE_THERMOSTATS:
-            return device_data
-
-        # Thermostat data (presets, temperatures etc)
-        device_data = self._device_data_climate(details, device_data)
-
-        return device_data
-
-
-class Smile(SmileComm, SmileData):
-    """The Plugwise SmileConnect class."""
-
-    # pylint: disable=too-many-instance-attributes, too-many-public-methods
-
-    def __init__(
-        self,
-        host: str,
-        password: str,
-        username: str = DEFAULT_USERNAME,
-        port: int = DEFAULT_PORT,
-        timeout: float = DEFAULT_TIMEOUT,
-        websession: aiohttp.ClientSession | None = None,
-    ) -> None:
-        """Set the constructor for this class."""
-        super().__init__(
-            host,
-            password,
-            username,
-            port,
-            timeout,
-            websession,
-        )
-        SmileData.__init__(self)
-
-        self.smile_hostname: str | None = None
-
-    async def connect(self) -> bool:
-        """Connect to Plugwise device and determine its name, type and version."""
-        result = await self._request(DOMAIN_OBJECTS)
-        # Work-around for Stretch fv 2.7.18
-        if not (vendor_names := result.findall("./module/vendor_name")):
-            result = await self._request(MODULES)
-            vendor_names = result.findall("./module/vendor_name")
-
-        names: list[str] = []
-        for name in vendor_names:
-            names.append(name.text)
-
-        vendor_models = result.findall("./module/vendor_model")
-        models: list[str] = []
-        for model in vendor_models:
-            models.append(model.text)
-
-        dsmrmain = result.find("./module/protocols/dsmrmain")
-        if "Plugwise" not in names and dsmrmain is None:  # pragma: no cover
-            LOGGER.error(
-                "Connected but expected text not returned, we got %s. Please create \
-                an issue on http://github.com/plugwise/python-plugwise",
-                result,
-            )
-            raise ResponseError
-
-        # Check if Anna is connected to an Adam
-        if "159.2" in models:
-            LOGGER.error(
-                "Your Anna is connected to an Adam, make sure to only add the Adam as integration."
-            )
-            raise InvalidSetupError
-
-        # Determine smile specifics
-        await self._smile_detect(result, dsmrmain)
-
-        # Update all endpoints on first connect
-        await self._full_update_device()
-
-        return True
-
-    async def _smile_detect_legacy(self, result: etree, dsmrmain: etree) -> str:
-        """Helper-function for _smile_detect()."""
-        # Stretch: find the MAC of the zigbee master_controller (= Stick)
-        if network := result.find("./module/protocols/master_controller"):
-            self.smile_zigbee_mac_address = network.find("mac_address").text
-        # Find the active MAC in case there is an orphaned Stick
-        if zb_networks := result.findall("./network"):
-            for zb_network in zb_networks:
-                if zb_network.find("./nodes/network_router"):
-                    network = zb_network.find("./master_controller")
-                    self.smile_zigbee_mac_address = network.find("mac_address").text
-
-        # Assume legacy
-        self._smile_legacy = True
-        # Try if it is a legacy Anna, assuming appliance thermostat,
-        # fake insert version assuming Anna, couldn't find another way to identify as legacy Anna
-        self.smile_fw_version = "1.8.0"
-        model = "smile_thermo"
-        if result.find('./appliance[type="thermostat"]') is None:
-            # It's a P1 legacy:
-            if dsmrmain is not None:
-                self._status = await self._request(STATUS)
-                self.smile_fw_version = self._status.find("./system/version").text
-                model = self._status.find("./system/product").text
-                self.smile_hostname = self._status.find("./network/hostname").text
-                self.smile_mac_address = self._status.find("./network/mac_address").text
-
-            # Or a legacy Stretch:
-            elif network is not None:
-                self._system = await self._request(SYSTEM)
-                self.smile_fw_version = self._system.find("./gateway/firmware").text
-                model = self._system.find("./gateway/product").text
-                self.smile_hostname = self._system.find("./gateway/hostname").text
-                # If wlan0 contains data it's active, so eth0 should be checked last
-                for network in ("wlan0", "eth0"):
-                    locator = f"./{network}/mac"
-                    if (net_locator := self._system.find(locator)) is not None:
-                        self.smile_mac_address = net_locator.text
-
-            else:  # pragma: no cover
-                # No cornercase, just end of the line
-                LOGGER.error(
-                    "Connected but no gateway device information found, please create \
-                     an issue on http://github.com/plugwise/python-plugwise"
-                )
-                raise ResponseError
-
-        return model
-
-    async def _smile_detect(self, result: etree, dsmrmain: etree) -> None:
-        """Helper-function for connect().
-
-        Detect which type of Smile is connected.
-        """
-        model: str | None = None
-        if (gateway := result.find("./gateway")) is not None:
-            model = gateway.find("vendor_model").text
-            self.smile_fw_version = gateway.find("firmware_version").text
-            self.smile_hw_version = gateway.find("hardware_version").text
-            self.smile_hostname = gateway.find("hostname").text
-            self.smile_mac_address = gateway.find("mac_address").text
-        else:
-            model = await self._smile_detect_legacy(result, dsmrmain)
-
-        if model is None or self.smile_fw_version is None:  # pragma: no cover
-            # Corner case check
-            LOGGER.error(
-                "Unable to find model or version information, please create \
-                 an issue on http://github.com/plugwise/python-plugwise"
-            )
-            raise UnsupportedDeviceError
-
-        ver = semver.VersionInfo.parse(self.smile_fw_version)
-        target_smile = f"{model}_v{ver.major}"
-        LOGGER.debug("Plugwise identified as %s", target_smile)
-        if target_smile not in SMILES:
-            LOGGER.error(
-                'Your version Smile identified as "%s" seems unsupported by our plugin, please \
-                create an issue on http://github.com/plugwise/python-plugwise',
-                target_smile,
-            )
-            raise UnsupportedDeviceError
-
-        self.smile_model = "Gateway"
-        self.smile_name = SMILES[target_smile].smile_name
-        self.smile_type = SMILES[target_smile].smile_type
-        self.smile_version = (self.smile_fw_version, ver)
-
-        if target_smile in LEGACY_SMILES:
-            self._smile_legacy = True
-
-        if self.smile_type == "stretch":
-            self._stretch_v2 = self.smile_version[1].major == 2
-            self._stretch_v3 = self.smile_version[1].major == 3
-
-        self._is_thermostat = self.smile_type == "thermostat"
-
-    async def _full_update_device(self) -> None:
-        """Perform a first fetch of all XML data, needed for initialization."""
-        self._locations = await self._request(LOCATIONS)
-        self._modules = await self._request(MODULES)
-
-        # P1 legacy has no appliances and nothing of interest in domain_objects
-        if not (self.smile_type == "power" and self._smile_legacy):
-            self._appliances = await self._request(APPLIANCES)
-            await self._update_domain_objects()
-
-    async def _update_domain_objects(self) -> None:
-        """Helper-function for smile.py: full_update_device() and async_update().
-
-        Request domain_objects data.
-        """
-        self._domain_objects = await self._request(DOMAIN_OBJECTS)
-
-        # If Plugwise notifications present:
-        self._notifications = {}
-        for notification in self._domain_objects.findall("./notification"):
-            try:
-                msg_id = notification.attrib["id"]
-                msg_type = notification.find("type").text
-                msg = notification.find("message").text
-                self._notifications.update({msg_id: {msg_type: msg}})
-                LOGGER.debug("Plugwise notifications: %s", self._notifications)
-            except AttributeError:  # pragma: no cover
-                LOGGER.debug(
-                    "Plugwise notification present but unable to process, manually investigate: %s",
-                    f"{self._endpoint}{DOMAIN_OBJECTS}",
-                )
-
-    async def async_update(self) -> tuple[GatewayData, dict[str, DeviceData]]:
-        """Perform an incremental update for updating the various device states."""
-        if self.smile_type != "power":
-            await self._update_domain_objects()
-        elif not self._smile_legacy:
-            self._locations = await self._request(LOCATIONS)
-        else:
-            self._modules = await self._request(MODULES)
-
-        # P1 legacy has no appliances
-        if not (self.smile_type == "power" and self._smile_legacy):
-            self._appliances = await self._request(APPLIANCES)
-
-        self.gw_data["notifications"] = self._notifications
-
-        for dev_id, dev_dict in self.gw_devices.items():
-            data = self._get_device_data(dev_id)
-            for key, value in data.items():
-                if key in dev_dict:
-                    dev_dict[key] = value  # type: ignore [literal-required]
-
-            for item in ("binary_sensors", "sensors", "switches"):
-                notifs: dict[str, dict[str, str]] = {}
-                if item == "binary_sensors":
-                    notifs = self._notifications
-                if item in dev_dict:
-                    for key in data:
-                        update_helper(
-                            data,
-                            self.gw_devices,
-                            dev_dict,
-                            dev_id,
-                            item,
-                            key,
-                            notifs,
-                        )
-
-            # Update for cooling
-            self.update_for_cooling(dev_dict)
-
-        return (self.gw_data, self.gw_devices)
-
-    async def _set_schedule_state_legacy(
-        self, loc_id: str, name: str, status: str
-    ) -> None:
-        """Helper-function for set_schedule_state()."""
-        schedule_rule_id: str | None = None
-        for rule in self._domain_objects.findall("rule"):
-            if rule.find("name").text == name:
-                schedule_rule_id = rule.attrib["id"]
-
-        if schedule_rule_id is None:
-            raise PlugwiseError("Plugwise: no schedule with this name available.")
-
-        new_state = "false"
-        if status == "on":
-            new_state = "true"
-        # If no state change is requested, do nothing
-        if new_state == self._schedule_old_states[loc_id][name]:
-            return
-
-        locator = f'.//*[@id="{schedule_rule_id}"]/template'
-        for rule in self._domain_objects.findall(locator):
-            template_id = rule.attrib["id"]
-
-        uri = f"{RULES};id={schedule_rule_id}"
-        data = (
-            "<rules><rule"
-            f' id="{schedule_rule_id}"><name><![CDATA[{name}]]></name><template'
-            f' id="{template_id}" /><active>{new_state}</active></rule></rules>'
-        )
-
-        await self._request(uri, method="put", data=data)
-        self._schedule_old_states[loc_id][name] = new_state
-
-    async def set_schedule_state(
-        self, loc_id: str, name: str | None, new_state: str
-    ) -> None:
-        """Activate/deactivate the Schedule, with the given name, on the relevant Thermostat.
-
-        Determined from - DOMAIN_OBJECTS.
-        In HA Core used to set the hvac_mode: in practice switch between schedule on - off.
-        """
-        # Input checking
-        if new_state not in ["on", "off"]:
-            raise PlugwiseError("Plugwise: invalid schedule state.")
-        if name is None:
-            raise PlugwiseError(
-                "Plugwise: cannot change schedule-state: no schedule name provided"
-            )
-
-        if self._smile_legacy:
-            await self._set_schedule_state_legacy(loc_id, name, new_state)
-            return
-
-        schedule_rule = self._rule_ids_by_name(name, loc_id)
-        # Raise an error when the schedule name does not exist
-        if not schedule_rule or schedule_rule is None:
-            raise PlugwiseError("Plugwise: no schedule with this name available.")
-
-        # If no state change is requested, do nothing
-        if new_state == self._schedule_old_states[loc_id][name]:
-            return
-
-        schedule_rule_id: str = next(iter(schedule_rule))
-
-        template = (
-            '<template tag="zone_preset_based_on_time_and_presence_with_override" />'
-        )
-        if self.smile_name != "Adam":
-            locator = f'.//*[@id="{schedule_rule_id}"]/template'
-            template_id = self._domain_objects.find(locator).attrib["id"]
-            template = f'<template id="{template_id}" />'
-
-        locator = f'.//*[@id="{schedule_rule_id}"]/contexts'
-        contexts = self._domain_objects.find(locator)
-        locator = f'.//*[@id="{loc_id}"].../...'
-        if (subject := contexts.find(locator)) is None:
-            subject = f'<context><zone><location id="{loc_id}" /></zone></context>'
-            subject = etree.fromstring(subject)
-
-        if new_state == "off":
-            self._last_active[loc_id] = name
-            contexts.remove(subject)
-        if new_state == "on":
-            contexts.append(subject)
-
-        contexts = etree.tostring(contexts, encoding="unicode").rstrip()
-
-        uri = f"{RULES};id={schedule_rule_id}"
-        data = (
-            f'<rules><rule id="{schedule_rule_id}"><name><![CDATA[{name}]]></name>'
-            f"{template}{contexts}</rule></rules>"
-        )
-        await self._request(uri, method="put", data=data)
-        self._schedule_old_states[loc_id][name] = new_state
-
-    async def _set_preset_legacy(self, preset: str) -> None:
-        """Set the given Preset on the relevant Thermostat - from DOMAIN_OBJECTS."""
-        locator = f'rule/directives/when/then[@icon="{preset}"].../.../...'
-        rule = self._domain_objects.find(locator)
-        data = f'<rules><rule id="{rule.attrib["id"]}"><active>true</active></rule></rules>'
-
-        await self._request(RULES, method="put", data=data)
-
-    async def set_preset(self, loc_id: str, preset: str) -> None:
-        """Set the given Preset on the relevant Thermostat - from LOCATIONS."""
-        if (presets := self._presets(loc_id)) is None:
-            raise PlugwiseError("Plugwise: no presets available.")  # pragma: no cover
-        if preset not in list(presets):
-            raise PlugwiseError("Plugwise: invalid preset.")
-
-        if self._smile_legacy:
-            await self._set_preset_legacy(preset)
-            return
-
-        current_location = self._locations.find(f'location[@id="{loc_id}"]')
-        location_name = current_location.find("name").text
-        location_type = current_location.find("type").text
-
-        uri = f"{LOCATIONS};id={loc_id}"
-        data = (
-            "<locations><location"
-            f' id="{loc_id}"><name>{location_name}</name><type>{location_type}'
-            f"</type><preset>{preset}</preset></location></locations>"
-        )
-
-        await self._request(uri, method="put", data=data)
-
-    async def set_temperature(self, loc_id: str, items: dict[str, float]) -> None:
-        """Set the given Temperature on the relevant Thermostat."""
-        setpoint: float | None = None
-        if "setpoint" in items:
-            setpoint = items["setpoint"]
-        if self._cooling_present:
-            if "setpoint_low" in items:
-                setpoint = items["setpoint_low"]
-            if self._cooling_active:
-                if "setpoint_high" in items:
-                    setpoint = items["setpoint_high"]
-
-        if setpoint is None:
-            raise PlugwiseError(
-                "Plugwise: failed setting temperature: no valid input provided"
-            )  # pragma: no cover"
-
-        temperature = str(setpoint)
-        uri = self._thermostat_uri(loc_id)
-        data = (
-            "<thermostat_functionality><setpoint>"
-            f"{temperature}</setpoint></thermostat_functionality>"
-        )
-
-        await self._request(uri, method="put", data=data)
-
-    async def set_number_setpoint(self, key: str, temperature: float) -> None:
-        """Set the max. Boiler or DHW setpoint on the Central Heating boiler."""
-        temp = str(temperature)
-        thermostat_id: str | None = None
-        locator = f'appliance[@id="{self._heater_id}"]/actuator_functionalities/thermostat_functionality'
-        if th_func_list := self._appliances.findall(locator):
-            for th_func in th_func_list:
-                if th_func.find("type").text == key:
-                    thermostat_id = th_func.attrib["id"]
-
-        if thermostat_id is None:
-            raise PlugwiseError(f"Plugwise: cannot change setpoint, {key} not found.")
-
-        uri = f"{APPLIANCES};id={self._heater_id}/thermostat;id={thermostat_id}"
-        data = f"<thermostat_functionality><setpoint>{temp}</setpoint></thermostat_functionality>"
-        await self._request(uri, method="put", data=data)
-
-    async def _set_groupswitch_member_state(
-        self, members: list[str], state: str, switch: Munch
-    ) -> None:
-        """Helper-function for set_switch_state().
-
-        Set the given State of the relevant Switch within a group of members.
-        """
-        for member in members:
-            locator = f'appliance[@id="{member}"]/{switch.actuator}/{switch.func_type}'
-            switch_id = self._appliances.find(locator).attrib["id"]
-            uri = f"{APPLIANCES};id={member}/{switch.device};id={switch_id}"
-            if self._stretch_v2:
-                uri = f"{APPLIANCES};id={member}/{switch.device}"
-            data = f"<{switch.func_type}><{switch.func}>{state}</{switch.func}></{switch.func_type}>"
-
-            await self._request(uri, method="put", data=data)
-
-    async def set_switch_state(
-        self, appl_id: str, members: list[str] | None, model: str, state: str
-    ) -> None:
-        """Set the given State of the relevant Switch."""
-        switch = Munch()
-        switch.actuator = "actuator_functionalities"
-        switch.device = "relay"
-        switch.func_type = "relay_functionality"
-        switch.func = "state"
-        if model == "dhw_cm_switch":
-            switch.device = "toggle"
-            switch.func_type = "toggle_functionality"
-            switch.act_type = "domestic_hot_water_comfort_mode"
-
-        if model == "cooling_ena_switch":
-            switch.device = "toggle"
-            switch.func_type = "toggle_functionality"
-            switch.act_type = "cooling_enabled"
-
-        if model == "lock":
-            switch.func = "lock"
-            state = "false" if state == "off" else "true"
-
-        if self._stretch_v2:
-            switch.actuator = "actuators"
-            switch.func_type = "relay"
-
-        if members is not None:
-            return await self._set_groupswitch_member_state(members, state, switch)
-
-        locator = f'appliance[@id="{appl_id}"]/{switch.actuator}/{switch.func_type}'
-        found: list[etree] = self._appliances.findall(locator)
-        for item in found:
-            if (sw_type := item.find("type")) is not None:
-                if sw_type.text == switch.act_type:
-                    switch_id = item.attrib["id"]
-            else:
-                switch_id = item.attrib["id"]
-                break
-
-        uri = f"{APPLIANCES};id={appl_id}/{switch.device};id={switch_id}"
-        if self._stretch_v2:
-            uri = f"{APPLIANCES};id={appl_id}/{switch.device}"
-        data = f"<{switch.func_type}><{switch.func}>{state}</{switch.func}></{switch.func_type}>"
-
-        if model == "relay":
-            locator = (
-                f'appliance[@id="{appl_id}"]/{switch.actuator}/{switch.func_type}/lock'
-            )
-            # Don't bother switching a relay when the corresponding lock-state is true
-            if self._appliances.find(locator).text == "true":
-                raise PlugwiseError("Plugwise: the locked Relay was not switched.")
-
-        await self._request(uri, method="put", data=data)
-
-    async def set_regulation_mode(self, mode: str) -> None:
-        """Set the heating regulation mode."""
-        if mode not in self._reg_allowed_modes:
-            raise PlugwiseError("Plugwise: invalid regulation mode.")
-
-        uri = f"{APPLIANCES};type=gateway/regulation_mode_control"
-        duration = ""
-        if "bleeding" in mode:
-            duration = "<duration>300</duration>"
-        data = f"<regulation_mode_control_functionality>{duration}<mode>{mode}</mode></regulation_mode_control_functionality>"
-
-        await self._request(uri, method="put", data=data)
-
-    async def set_dhw_mode(self, mode: str) -> None:
-        """Set the domestic hot water heating regulation mode."""
-        if mode not in self._dhw_allowed_modes:
-            raise PlugwiseError("Plugwise: invalid dhw mode.")
-
-        uri = f"{APPLIANCES};type=heater_central/domestic_hot_water_mode_control"
-        data = f"<domestic_hot_water_mode_control_functionality><mode>{mode}</mode></domestic_hot_water_mode_control_functionality>"
-
-        await self._request(uri, method="put", data=data)
-
-    async def delete_notification(self) -> None:
-        """Delete the active Plugwise Notification."""
-        await self._request(NOTIFICATIONS, method="delete")
diff --git a/plugwise/stick/__init__.py b/plugwise/stick/__init__.py
deleted file mode 100644
index bf2e0c08f..000000000
--- a/plugwise/stick/__init__.py
+++ /dev/null
@@ -1,759 +0,0 @@
-"""
-Use of this source code is governed by the MIT license found in the LICENSE file.
-
-Main stick object to control associated plugwise plugs
-"""
-from datetime import datetime, timedelta
-import logging
-import sys
-import threading
-import time
-
-from .constants import (
-    ACCEPT_JOIN_REQUESTS,
-    CB_JOIN_REQUEST,
-    CB_NEW_NODE,
-    HW_MODELS,
-    MESSAGE_TIME_OUT,
-    NODE_TYPE_CELSIUS_NR,
-    NODE_TYPE_CELSIUS_SED,
-    NODE_TYPE_CIRCLE,
-    NODE_TYPE_CIRCLE_PLUS,
-    NODE_TYPE_SCAN,
-    NODE_TYPE_SENSE,
-    NODE_TYPE_STEALTH,
-    NODE_TYPE_SWITCH,
-    PRIORITY_LOW,
-    STATE_ACTIONS,
-    UTF8_DECODE,
-    WATCHDOG_DEAMON,
-)
-from .controller import StickMessageController
-from .exceptions import (
-    CirclePlusError,
-    NetworkDown,
-    PortError,
-    StickInitError,
-    TimeoutException,
-)
-from .messages.requests import (
-    NodeAddRequest,
-    NodeAllowJoiningRequest,
-    NodeInfoRequest,
-    NodePingRequest,
-    NodeRemoveRequest,
-    StickInitRequest,
-)
-from .messages.responses import (
-    NodeAckLargeResponse,
-    NodeAckResponse,
-    NodeInfoResponse,
-    NodeJoinAvailableResponse,
-    NodeRemoveResponse,
-    NodeResponse,
-    StickInitResponse,
-)
-from .nodes.circle import PlugwiseCircle
-from .nodes.circle_plus import PlugwiseCirclePlus
-from .nodes.scan import PlugwiseScan
-from .nodes.sense import PlugwiseSense
-from .nodes.stealth import PlugwiseStealth
-from .util import validate_mac
-
-_LOGGER = logging.getLogger(__name__)
-
-
-class Stick:
-    """Plugwise connection stick."""
-
-    def __init__(self, port, callback=None):
-        self.circle_plus_mac = None
-        self.init_callback = None
-        self.msg_controller = None
-        self.scan_callback = None
-
-        self._accept_join_requests = ACCEPT_JOIN_REQUESTS
-        self._auto_update_manually = False
-        self._auto_update_timer = 0
-        self._circle_plus_discovered = False
-        self._circle_plus_retries = 0
-        self._device_nodes = {}
-        self._joined_nodes = 0
-        self._mac_stick = None
-        self._messages_for_undiscovered_nodes = []
-        self._network_id = None
-        self._network_online = False
-        self._nodes_discovered = None
-        self._nodes_not_discovered = {}
-        self._nodes_off_line = 0
-        self._nodes_to_discover = {}
-        self._port = port
-        self._run_update_thread = False
-        self._run_watchdog = None
-        self._stick_callbacks = {}
-        self._stick_initialized = False
-        self._update_thread = None
-        self._watchdog_thread = None
-
-        if callback:
-            self.auto_initialize(callback)
-
-    @property
-    def devices(self) -> dict:
-        """All discovered and supported plugwise devices with the MAC address as their key"""
-        return self._device_nodes
-
-    @property
-    def joined_nodes(self) -> int:
-        """Return total number of nodes registered to Circle+ including Circle+ itself."""
-        return self._joined_nodes + 1
-
-    @property
-    def mac(self) -> str:
-        """Return the MAC address of the USB-Stick."""
-        if self._mac_stick:
-            return self._mac_stick.decode(UTF8_DECODE)
-        return None
-
-    @property
-    def network_state(self) -> bool:
-        """Return the state of the Plugwise network."""
-        return self._network_online
-
-    @property
-    def network_id(self) -> int:
-        """Return the id of the Plugwise network."""
-        return self._network_id
-
-    @property
-    def port(self) -> str:
-        """Return currently configured port to USB-Stick."""
-        return self._port
-
-    @port.setter
-    def port(self, port: str):
-        """Set port to USB-Stick."""
-        if self.msg_controller:
-            self.disconnect()
-        self._port = port
-
-    def auto_initialize(self, callback=None):
-        """Automatic initialization of USB-stick and discovery of all registered nodes."""
-
-        def init_finished():
-            if not self._network_online:
-                _LOGGER.Error("plugwise Zigbee network down")
-            else:
-                self.scan(callback)
-
-        if not self.msg_controller:
-            self.msg_controller = StickMessageController(
-                self.port, self.message_processor, self.node_state_updates
-            )
-        try:
-            self.msg_controller.connect_to_stick()
-            self.initialize_stick(init_finished)
-        except PortError as err:
-            _LOGGER.error("Failed to connect: '%s'", err)
-        except StickInitError as err:
-            _LOGGER.error("Failed to initialize USBstick: '%s'", err)
-        except NetworkDown:
-            _LOGGER.error("Failed to communicated: Plugwise Zigbee network")
-        except TimeoutException:
-            _LOGGER.error("Timeout exception while initializing USBstick")
-        except Exception as err:  # pylint: disable=broad-except
-            _LOGGER.error("Unknown error : %s", err)
-
-    def connect(self, callback=None):
-        """Startup message controller and connect to stick."""
-        if not self.msg_controller:
-            self.msg_controller = StickMessageController(
-                self.port, self.message_processor, self.node_state_updates
-            )
-        if self.msg_controller.connect_to_stick(callback):
-            # update daemon
-            self._run_update_thread = False
-            self._auto_update_timer = 0
-            self._update_thread = threading.Thread(
-                None, self._update_loop, "update_thread", (), {}
-            )
-            self._update_thread.daemon = True
-
-    def initialize_stick(self, callback=None, timeout=MESSAGE_TIME_OUT):
-        """Initialize the USB-stick, start watchdog thread and raise an error if this fails."""
-        if not self.msg_controller.connection.is_connected():
-            raise StickInitError
-        _LOGGER.debug("Send init request to Plugwise Zigbee stick")
-        self.msg_controller.send(StickInitRequest(), callback)
-        time_counter = 0
-        while not self._stick_initialized and (time_counter < timeout):
-            time_counter += 0.1
-            time.sleep(0.1)
-        if not self._stick_initialized:
-            raise StickInitError
-        if not self._network_online:
-            raise NetworkDown
-
-    def initialize_circle_plus(self, callback=None, timeout=MESSAGE_TIME_OUT):
-        """Initialize connection from USB-Stick to the Circle+/Stealth+ node and raise an error if this fails."""
-        if (
-            not self.msg_controller.connection.is_connected()
-            or not self._stick_initialized
-            or not self.circle_plus_mac
-        ):
-            raise StickInitError
-        self.discover_node(self.circle_plus_mac, callback)
-
-        time_counter = 0
-        while not self._circle_plus_discovered and (time_counter < timeout):
-            time_counter += 0.1
-            time.sleep(0.1)
-        if not self._circle_plus_discovered:
-            raise CirclePlusError
-
-    def disconnect(self):
-        """Disconnect from stick and raise error if it fails"""
-        self._run_watchdog = False
-        self._run_update_thread = False
-        self._auto_update_timer = 0
-        if self.msg_controller:
-            self.msg_controller.disconnect_from_stick()
-        self.msg_controller = None
-
-    def subscribe_stick_callback(self, callback, callback_type):
-        """Subscribe callback to execute."""
-        if callback_type not in self._stick_callbacks:
-            self._stick_callbacks[callback_type] = []
-        self._stick_callbacks[callback_type].append(callback)
-
-    def unsubscribe_stick_callback(self, callback, callback_type):
-        """Register callback to execute."""
-        if callback_type in self._stick_callbacks:
-            self._stick_callbacks[callback_type].remove(callback)
-
-    def allow_join_requests(self, enable: bool, accept: bool):
-        """
-        Enable or disable Plugwise network
-        Automatically accept new join request
-        """
-        self.msg_controller.send(NodeAllowJoiningRequest(enable))
-        if enable:
-            self._accept_join_requests = accept
-        else:
-            self._accept_join_requests = False
-
-    def scan(self, callback=None):
-        """Scan and try to detect all registered nodes."""
-        self.scan_callback = callback
-        self.scan_for_registered_nodes()
-
-    def scan_circle_plus(self):
-        """Scan the Circle+ memory for registered nodes."""
-        if self._device_nodes.get(self.circle_plus_mac):
-            _LOGGER.debug("Scan Circle+ for linked nodes...")
-            self._device_nodes[self.circle_plus_mac].scan_for_nodes(self.discover_nodes)
-        else:
-            _LOGGER.error("Circle+ is not discovered yet")
-
-    def scan_for_registered_nodes(self):
-        """Discover Circle+ and all registered nodes at Circle+."""
-        if self.circle_plus_mac:
-            if self._device_nodes.get(self.circle_plus_mac):
-                self.scan_circle_plus()
-            else:
-                _LOGGER.debug("Discover Circle+ at %s", self.circle_plus_mac)
-                self.discover_node(self.circle_plus_mac, self.scan_circle_plus)
-        else:
-            _LOGGER.error(
-                "Plugwise stick not properly initialized, Circle+ MAC is missing."
-            )
-
-    def discover_nodes(self, nodes_to_discover):
-        """Helper to discover all registered nodes."""
-        _LOGGER.debug("Scan plugwise network finished")
-        self._nodes_discovered = 0
-        self._nodes_to_discover = nodes_to_discover
-        self._joined_nodes = len(nodes_to_discover)
-
-        # setup timeout for node discovery
-        discover_timeout = 10 + (len(nodes_to_discover) * 2) + (MESSAGE_TIME_OUT)
-        threading.Timer(discover_timeout, self.scan_timeout_expired).start()
-        _LOGGER.debug("Start discovery of linked node types...")
-        for mac in nodes_to_discover:
-            self.discover_node(mac, self.node_discovered_by_scan)
-
-    def node_discovered_by_scan(self, nodes_off_line=False):
-        """Node discovered by initial scan."""
-        if nodes_off_line:
-            self._nodes_off_line += 1
-        self._nodes_discovered += 1
-        _LOGGER.debug(
-            "Discovered Plugwise node %s (%s off-line) of %s",
-            str(len(self._device_nodes)),
-            str(self._nodes_off_line),
-            str(len(self._nodes_to_discover)),
-        )
-        if (len(self._device_nodes) - 1 + self._nodes_off_line) >= len(
-            self._nodes_to_discover
-        ):
-            if self._nodes_off_line == 0:
-                self._nodes_to_discover = {}
-                self._nodes_not_discovered = {}
-            else:
-                for mac in self._nodes_to_discover:
-                    if not self._device_nodes.get(mac):
-                        _LOGGER.info(
-                            "Failed to discover node type for registered MAC '%s'. This is expected for battery powered nodes, they will be discovered at their first awake",
-                            str(mac),
-                        )
-                    else:
-                        if mac in self._nodes_not_discovered:
-                            del self._nodes_not_discovered[mac]
-            self.msg_controller.discovery_finished = True
-            if self.scan_callback:
-                self.scan_callback()
-
-    def scan_timeout_expired(self):
-        """Timeout for initial scan."""
-        if not self.msg_controller.discovery_finished:
-            for mac in self._nodes_to_discover:
-                #  TODO: 20220206 is there 'mac' in the dict? Otherwise it can be rewritten as below (twice as fast above .get)
-                #        if mac not in self._device_nodes:
-                if not self._device_nodes.get(mac):
-                    _LOGGER.info(
-                        "Failed to discover node type for registered MAC '%s'. This is expected for battery powered nodes, they will be discovered at their first awake",
-                        str(mac),
-                    )
-                else:
-                    if mac in self._nodes_not_discovered:
-                        del self._nodes_not_discovered[mac]
-            if self.scan_callback:
-                self.scan_callback()
-
-    def _append_node(self, mac, address, node_type):
-        """Add node to list of controllable nodes"""
-        _LOGGER.debug(
-            "Add new node type (%s) with mac %s",
-            str(node_type),
-            mac,
-        )
-        if node_type == NODE_TYPE_CIRCLE_PLUS:
-            self._device_nodes[mac] = PlugwiseCirclePlus(
-                mac, address, self.msg_controller.send
-            )
-        elif node_type == NODE_TYPE_CIRCLE:
-            self._device_nodes[mac] = PlugwiseCircle(
-                mac, address, self.msg_controller.send
-            )
-        elif node_type == NODE_TYPE_SWITCH:
-            self._device_nodes[mac] = None
-        elif node_type == NODE_TYPE_SENSE:
-            self._device_nodes[mac] = PlugwiseSense(
-                mac, address, self.msg_controller.send
-            )
-        elif node_type == NODE_TYPE_SCAN:
-            self._device_nodes[mac] = PlugwiseScan(
-                mac, address, self.msg_controller.send
-            )
-        elif node_type == NODE_TYPE_CELSIUS_SED:
-            self._device_nodes[mac] = None
-        elif node_type == NODE_TYPE_CELSIUS_NR:
-            self._device_nodes[mac] = None
-        elif node_type == NODE_TYPE_STEALTH:
-            self._device_nodes[mac] = PlugwiseStealth(
-                mac, address, self.msg_controller.send
-            )
-        else:
-            _LOGGER.warning("Unsupported node type '%s'", str(node_type))
-            self._device_nodes[mac] = None
-
-        # process previous missed messages
-        msg_to_process = self._messages_for_undiscovered_nodes[:]
-        self._messages_for_undiscovered_nodes = []
-        for msg in msg_to_process:
-            self.message_processor(msg)
-
-    def node_state_updates(self, mac, state: bool):
-        """Update availability state of a node"""
-        if self._device_nodes.get(mac):
-            if not self._device_nodes[mac].battery_powered:
-                self._device_nodes[mac].available = state
-
-    def node_join(self, mac: str, callback=None) -> bool:
-        """Accept node to join Plugwise network by register mac in Circle+ memory"""
-        if validate_mac(mac):
-            self.msg_controller.send(
-                NodeAddRequest(bytes(mac, UTF8_DECODE), True), callback
-            )
-            return True
-        _LOGGER.warning("Invalid mac '%s' address, unable to join node manually.", mac)
-        return False
-
-    def node_unjoin(self, mac: str, callback=None) -> bool:
-        """Remove node from the Plugwise network by deleting mac from the Circle+ memory"""
-        if validate_mac(mac):
-            self.msg_controller.send(
-                NodeRemoveRequest(bytes(self.circle_plus_mac, UTF8_DECODE), mac),
-                callback,
-            )
-            return True
-
-        _LOGGER.warning(
-            "Invalid mac '%s' address, unable to unjoin node manually.", mac
-        )
-        return False
-
-    def _remove_node(self, mac):
-        """Remove node from list of controllable nodes."""
-        if self._device_nodes.get(mac):
-            del self._device_nodes[mac]
-        else:
-            _LOGGER.warning("Node %s does not exists, unable to remove node.", mac)
-
-    def message_processor(self, message: NodeResponse):
-        """Received message from Plugwise network."""
-        mac = message.mac.decode(UTF8_DECODE)
-        if isinstance(message, (NodeAckLargeResponse, NodeAckResponse)):
-            if message.ack_id in STATE_ACTIONS:
-                self._pass_message_to_node(message, mac)
-        elif isinstance(message, NodeInfoResponse):
-            self._process_node_info_response(message, mac)
-        elif isinstance(message, StickInitResponse):
-            self._process_stick_init_response(message)
-        elif isinstance(message, NodeJoinAvailableResponse):
-            self._process_node_join_request(message, mac)
-        elif isinstance(message, NodeRemoveResponse):
-            self._process_node_remove(message)
-        else:
-            self._pass_message_to_node(message, mac)
-
-    def _process_stick_init_response(self, stick_init_response: StickInitResponse):
-        """Process StickInitResponse message."""
-        self._mac_stick = stick_init_response.mac
-        if stick_init_response.network_is_online.value == 1:
-            self._network_online = True
-        else:
-            self._network_online = False
-        # Replace first 2 characters by 00 for mac of circle+ node
-        self.circle_plus_mac = "00" + stick_init_response.circle_plus_mac.value[
-            2:
-        ].decode(UTF8_DECODE)
-        self._network_id = stick_init_response.network_id.value
-        self._stick_initialized = True
-        if not self._run_watchdog:
-            self._run_watchdog = True
-            self._watchdog_thread = threading.Thread(
-                None, self._watchdog_loop, "watchdog_thread", (), {}
-            )
-            self._watchdog_thread.daemon = True
-            self._watchdog_thread.start()
-
-    def _process_node_info_response(self, node_info_response, mac):
-        """Process NodeInfoResponse message."""
-        if not self._pass_message_to_node(node_info_response, mac, False):
-            _LOGGER.debug(
-                "Received NodeInfoResponse from currently unknown node with mac %s with sequence id %s",
-                mac,
-                str(node_info_response.seq_id),
-            )
-            if node_info_response.node_type.value == NODE_TYPE_CIRCLE_PLUS:
-                self._circle_plus_discovered = True
-                self._append_node(mac, 0, node_info_response.node_type.value)
-                if mac in self._nodes_not_discovered:
-                    del self._nodes_not_discovered[mac]
-            else:
-                if mac in self._nodes_to_discover:
-                    _LOGGER.info(
-                        "Node with mac %s discovered",
-                        mac,
-                    )
-                    self._append_node(
-                        mac,
-                        self._nodes_to_discover[mac],
-                        node_info_response.node_type.value,
-                    )
-            self._pass_message_to_node(node_info_response, mac)
-
-    def _process_node_join_request(self, node_join_request, mac):
-        """
-        Process NodeJoinAvailableResponse message from a node that
-        is not part of a plugwise network yet and wants to join
-        """
-        if self._device_nodes.get(mac):
-            _LOGGER.debug(
-                "Received node available message for node %s which is already joined.",
-                mac,
-            )
-        else:
-            if self._accept_join_requests:
-                # Send accept join request
-                _LOGGER.info(
-                    "Accepting network join request for node with mac %s",
-                    mac,
-                )
-                self.msg_controller.send(NodeAddRequest(node_join_request.mac, True))
-                self._nodes_not_discovered[mac] = (None, None)
-            else:
-                _LOGGER.debug(
-                    "New node with mac %s requesting to join Plugwise network, do callback",
-                    mac,
-                )
-                self.do_callback(CB_JOIN_REQUEST, mac)
-
-    def _process_node_remove(self, node_remove_response):
-        """
-        Process NodeRemoveResponse message with confirmation
-        if node is is removed from the Plugwise network.
-        """
-        unjoined_mac = node_remove_response.node_mac_id.value
-        if node_remove_response.status.value == 1:
-            if self._device_nodes.get(unjoined_mac):
-                del self._device_nodes[unjoined_mac]
-                _LOGGER.info(
-                    "Received NodeRemoveResponse from node %s it has been unjoined from Plugwise network",
-                    unjoined_mac,
-                )
-            else:
-                _LOGGER.debug(
-                    "Unknown node with mac %s has been unjoined from Plugwise network",
-                    unjoined_mac,
-                )
-        else:
-            _LOGGER.warning(
-                "Node with mac %s failed to unjoin from Plugwise network ",
-                unjoined_mac,
-            )
-
-    def _pass_message_to_node(self, message, mac, discover=True):
-        """
-        Pass message to node class to take action on message
-
-        Returns True if message has passed onto existing known node
-        """
-        if self._device_nodes.get(mac):
-            self._device_nodes[mac].message_for_node(message)
-            return True
-        if discover:
-            _LOGGER.info(
-                "Queue %s from %s because node is not discovered yet.",
-                message.__class__.__name__,
-                mac,
-            )
-            self._messages_for_undiscovered_nodes.append(message)
-            self.discover_node(mac, self._discover_after_scan, True)
-        return False
-
-    def _watchdog_loop(self):
-        """
-        Main worker loop to watch all other worker threads
-        """
-        time.sleep(5)
-        circle_plus_retry_counter = 0
-        while self._run_watchdog:
-            # Connection
-            if self.msg_controller.connection.is_connected():
-                # Connection reader daemon
-                if not self.msg_controller.connection.read_thread_alive():
-                    _LOGGER.warning("Unexpected halt of connection reader thread")
-                # Connection writer daemon
-                if not self.msg_controller.connection.write_thread_alive():
-                    _LOGGER.warning("Unexpected halt of connection writer thread")
-            # receive timeout daemon
-            if (
-                self.msg_controller.receive_timeout_thread_state
-                and self.msg_controller.receive_timeout_thread_is_alive
-            ):
-                self.msg_controller.restart_receive_timeout_thread()
-            # send message daemon
-            if (
-                self.msg_controller.send_message_thread_state
-                and self.msg_controller.send_message_thread_is_alive
-            ):
-                self.msg_controller.restart_send_message_thread()
-            # Update daemon
-            if self._run_update_thread:
-                if not self._update_thread.is_alive():
-                    _LOGGER.warning(
-                        "Unexpected halt of update thread, restart thread",
-                    )
-                    self._run_update_thread = True
-                    self._update_thread = threading.Thread(
-                        None, self._update_loop, "update_thread", (), {}
-                    )
-                    self._update_thread.daemon = True
-                    self._update_thread.start()
-            # Circle+ discovery
-            if not self._circle_plus_discovered:
-                # First hour every once an hour
-                if self._circle_plus_retries < 60 or circle_plus_retry_counter > 60:
-                    _LOGGER.info(
-                        "Circle+ not yet discovered, resubmit discovery request"
-                    )
-                    self.discover_node(self.circle_plus_mac, self.scan)
-                    self._circle_plus_retries += 1
-                    circle_plus_retry_counter = 0
-                circle_plus_retry_counter += 1
-            watchdog_loop_checker = 0
-            while watchdog_loop_checker < WATCHDOG_DEAMON and self._run_watchdog:
-                time.sleep(1)
-                watchdog_loop_checker += 1
-        _LOGGER.debug("watchdog loop stopped")
-
-    def _update_loop(self):
-        """
-        When node has not received any message during
-        last 2 update polls, reset availability
-        """
-        self._run_update_thread = True
-        _discover_counter = 0
-        _sync_clock = False
-        day_of_month = datetime.now().day
-        try:
-            while self._run_update_thread:
-                if datetime.now().day != day_of_month:
-                    day_of_month = datetime.now().day
-                    _sync_clock = True
-                for mac, device in self._device_nodes.items():
-                    if device:
-                        if device.battery_powered:
-                            # Check availability state of SED's
-                            self._check_availability_of_seds(mac)
-                        else:
-                            # Do ping request for all non SED's
-                            device.do_ping()
-
-                        if device.measures_power:
-                            # Request current power usage
-                            device.request_power_update()
-                            # Sync internal clock of power measure nodes once a day
-                            if _sync_clock:
-                                device.sync_clock()
-                _sync_clock = False
-
-                # Do a single ping for undiscovered nodes once per 10 update cycles
-                if _discover_counter == 10:
-                    for mac in self._nodes_not_discovered:
-                        self.msg_controller.send(
-                            NodePingRequest(bytes(mac, UTF8_DECODE)),
-                            None,
-                            -1,
-                            PRIORITY_LOW,
-                        )
-                    _discover_counter = 0
-                else:
-                    _discover_counter += 1
-
-                if self._auto_update_timer and self._run_update_thread:
-                    update_loop_checker = 0
-                    while (
-                        update_loop_checker < self._auto_update_timer
-                        and self._run_update_thread
-                    ):
-                        time.sleep(1)
-                        update_loop_checker += 1
-
-        # TODO: narrow exception
-        except Exception as err:  # pylint: disable=broad-except
-            _exc_type, _exc_obj, exc_tb = sys.exc_info()
-            _LOGGER.error(
-                "Error at line %s of _update_loop : %s", exc_tb.tb_lineno, err
-            )
-        _LOGGER.debug("Update loop stopped")
-
-    def auto_update(self, timer=None):
-        """Configure auto update polling daemon for power usage and availability state."""
-        if timer:
-            self._auto_update_timer = timer
-            self._auto_update_manually = True
-        elif timer == 0:
-            self._auto_update_timer = 0
-            self._run_update_thread = False
-        else:
-            # Timer based on a minimum of 5 seconds + 1 second for each node supporting power measurement
-            if not self._auto_update_manually:
-                count_nodes = 0
-                for _, node in self._device_nodes.items():
-                    if node.measures_power:
-                        count_nodes += 1
-                self._auto_update_timer = 5 + (count_nodes * 1)
-                _LOGGER.info(
-                    "Update interval is (re)set to %s seconds",
-                    str(self._auto_update_timer),
-                )
-        if not self._run_update_thread:
-            self._update_thread.start()
-
-    # Helper functions
-    def do_callback(self, callback_type, callback_arg=None):
-        """Helper to execute registered callbacks for specified callback type."""
-        if callback_type in self._stick_callbacks:
-            for callback in self._stick_callbacks[callback_type]:
-                try:
-                    if callback_arg is None:
-                        callback()
-                    else:
-                        callback(callback_arg)
-                # TODO: narrow exception
-                except Exception as err:  # pylint: disable=broad-except
-                    _LOGGER.error("Error while executing callback : %s", err)
-
-    def _check_availability_of_seds(self, mac):
-        """Helper to check if SED device is still sending its hartbeat."""
-        if self._device_nodes[mac].available:
-            if self._device_nodes[mac].last_update < (
-                datetime.now()
-                - timedelta(minutes=(self._device_nodes[mac].maintenance_interval + 1))
-            ):
-                _LOGGER.info(
-                    "No messages received within (%s minutes) of expected maintenance interval from node %s, mark as unavailable [%s > %s]",
-                    str(self._device_nodes[mac].maintenance_interval),
-                    mac,
-                    str(self._device_nodes[mac].last_update),
-                    str(
-                        datetime.now()
-                        - timedelta(
-                            minutes=(self._device_nodes[mac].maintenance_interval + 1)
-                        )
-                    ),
-                )
-                self._device_nodes[mac].available = False
-
-    def _discover_after_scan(self):
-        """Helper to do callback for new node."""
-        node_discovered = None
-        for mac in self._nodes_not_discovered:
-            if self._device_nodes.get(mac):
-                node_discovered = mac
-                break
-        if node_discovered:
-            del self._nodes_not_discovered[node_discovered]
-            self.do_callback(CB_NEW_NODE, node_discovered)
-            self.auto_update()
-
-    def discover_node(self, mac: str, callback=None, force_discover=False):
-        """Helper to try to discovery the node (type) based on mac."""
-        if not validate_mac(mac) or self._device_nodes.get(mac):
-            return
-        if mac not in self._nodes_not_discovered:
-            self._nodes_not_discovered[mac] = (
-                None,
-                None,
-            )
-            self.msg_controller.send(
-                NodeInfoRequest(bytes(mac, UTF8_DECODE)),
-                callback,
-            )
-        else:
-            (firstrequest, lastrequest) = self._nodes_not_discovered[mac]
-            if not (firstrequest and lastrequest):
-                self.msg_controller.send(
-                    NodeInfoRequest(bytes(mac, UTF8_DECODE)),
-                    callback,
-                    0,
-                    PRIORITY_LOW,
-                )
-            elif force_discover:
-                self.msg_controller.send(
-                    NodeInfoRequest(bytes(mac, UTF8_DECODE)),
-                    callback,
-                )
diff --git a/plugwise/stick/connections/__init__.py b/plugwise/stick/connections/__init__.py
deleted file mode 100644
index cd63debe9..000000000
--- a/plugwise/stick/connections/__init__.py
+++ /dev/null
@@ -1,136 +0,0 @@
-"""Base class for serial or socket connections to USB-Stick."""
-import logging
-import queue
-import threading
-import time
-
-from ..constants import SLEEP_TIME
-from ..messages.requests import NodeRequest
-
-_LOGGER = logging.getLogger(__name__)
-
-
-class StickConnection:
-    """Generic Plugwise stick connection."""
-
-    def __init__(self, port, parser):
-        """Initialize StickConnection."""
-        self.port = port
-        self.parser = parser
-        self.run_reader_thread = False
-        self.run_writer_thread = False
-        self._is_connected = False
-        self._writer = None
-
-        self._reader_thread = None
-        self._write_queue = None
-        self._writer_thread = None
-
-    ################################################
-    #               Open connection                #
-    ################################################
-
-    def connect(self) -> bool:
-        """Open the connection."""
-        if not self._is_connected:
-            self._open_connection()
-        return self._is_connected
-
-    def _open_connection(self):
-        """Placeholder."""
-
-    ################################################
-    #                     Reader                   #
-    ################################################
-
-    def _reader_start(self, name):
-        """Start the reader thread to receive data."""
-        self._reader_thread = threading.Thread(None, self._reader_deamon, name, (), {})
-        self.run_reader_thread = True
-        self._reader_thread.start()
-
-    def _reader_deamon(self):
-        """Thread to collect available data from connection."""
-        while self.run_reader_thread:
-            if data := self._read_data():
-                self.parser(data)
-            time.sleep(0.01)
-        _LOGGER.debug("Reader daemon stopped")
-
-    #  TODO: 20220125 function instead of self
-    def _read_data(self):
-        """placeholder."""
-        return b"0000"
-
-    ################################################
-    #                   Writer                     #
-    ################################################
-
-    def _writer_start(self, name: str):
-        """Start the writer thread to send data."""
-        self._write_queue = queue.Queue()
-        self._writer_thread = threading.Thread(None, self._writer_daemon, name, (), {})
-        self._writer_thread.daemon = True
-        self.run_writer_thread = True
-        self._writer_thread.start()
-
-    def _writer_daemon(self):
-        """Thread to write data from queue to existing connection."""
-        while self.run_writer_thread:
-            try:
-                (message, callback) = self._write_queue.get(block=True, timeout=1)
-            except queue.Empty:
-                time.sleep(SLEEP_TIME)
-            else:
-                _LOGGER.debug(
-                    "Sending %s to plugwise stick (%s)",
-                    message.__class__.__name__,
-                    message.serialize(),
-                )
-                self._write_data(message.serialize())
-                time.sleep(SLEEP_TIME)
-                if callback:
-                    callback()
-        _LOGGER.debug("Writer daemon stopped")
-
-    def _write_data(self, data):
-        """Placeholder."""
-
-    def send(self, message: NodeRequest, callback=None):
-        """Add message to write queue."""
-        self._write_queue.put_nowait((message, callback))
-
-    ################################################
-    #               Connection state               #
-    ################################################
-
-    def is_connected(self):
-        """Return connection state."""
-        return self._is_connected
-
-    def read_thread_alive(self):
-        """Return state of write thread."""
-        return self._reader_thread.is_alive() if self.run_reader_thread else False
-
-    def write_thread_alive(self):
-        """Return state of write thread."""
-        return self._writer_thread.is_alive() if self.run_writer_thread else False
-
-    ################################################
-    #               Close connection               #
-    ################################################
-
-    def disconnect(self):
-        """Close the connection."""
-        if self._is_connected:
-            self._is_connected = False
-            self.run_writer_thread = False
-            self.run_reader_thread = False
-            max_wait = 5 * SLEEP_TIME
-            while self._writer_thread.is_alive():
-                time.sleep(SLEEP_TIME)
-                max_wait -= SLEEP_TIME
-            self._close_connection()
-
-    def _close_connection(self):
-        """Placeholder."""
diff --git a/plugwise/stick/connections/serial.py b/plugwise/stick/connections/serial.py
deleted file mode 100644
index 082247627..000000000
--- a/plugwise/stick/connections/serial.py
+++ /dev/null
@@ -1,88 +0,0 @@
-"""Serial connection."""
-import logging
-
-import serial
-
-from ..connections import StickConnection
-from ..constants import BAUD_RATE, BYTE_SIZE, STOPBITS
-from ..exceptions import PortError
-
-_LOGGER = logging.getLogger(__name__)
-
-
-class PlugwiseUSBConnection(StickConnection):
-    """Simple wrapper around serial module."""
-
-    def __init__(self, port, parser):
-        super().__init__(port, parser)
-        self._baud = BAUD_RATE
-        self._byte_size = BYTE_SIZE
-        self._stopbits = STOPBITS
-        self._parity = serial.PARITY_NONE
-
-        self._serial = None
-
-    def _open_connection(self):
-        """Open serial port."""
-        _LOGGER.debug("Open serial port %s", self.port)
-        try:
-            self._serial = serial.Serial(
-                port=self.port,
-                baudrate=self._baud,
-                bytesize=self._byte_size,
-                parity=self._parity,
-                stopbits=self._stopbits,
-                timeout=1,
-            )
-        except serial.serialutil.SerialException as err:
-            _LOGGER.debug(
-                "Failed to connect to serial port %s, %s",
-                self.port,
-                err,
-            )
-            raise PortError(err)
-        self._is_connected = self._serial.isOpen()
-        if self._is_connected:
-            self._reader_start("serial_reader_thread")
-            self._writer_start("serial_writer_thread")
-            _LOGGER.debug("Successfully connected to serial port %s", self.port)
-        else:
-            _LOGGER.error(
-                "Failed to open serial port %s",
-                self.port,
-            )
-
-    def _close_connection(self):
-        """Close serial port."""
-        try:
-            self._serial.close()
-        except serial.serialutil.SerialException as err:
-            _LOGGER.debug(
-                "Failed to close serial port %s, %s",
-                self.port,
-                err,
-            )
-            raise PortError(err)
-
-    def _read_data(self):
-        """Read thread."""
-        if self._is_connected:
-            try:
-                serial_data = self._serial.read_all()
-            except serial.serialutil.SerialException as err:
-                _LOGGER.debug("Error while reading data from serial port : %s", err)
-                self._is_connected = False
-                raise PortError(err)
-            except Exception as err:  # pylint: disable=broad-except
-                _LOGGER.debug("Error _read_data : %s", err)
-            return serial_data
-        return None
-
-    def _write_data(self, data):
-        """Write data to serial port."""
-        try:
-            self._serial.write(data)
-        except serial.serialutil.SerialException as err:
-            _LOGGER.debug("Error while writing data to serial port : %s", err)
-            self._is_connected = False
-            raise PortError(err)
diff --git a/plugwise/stick/connections/socket.py b/plugwise/stick/connections/socket.py
deleted file mode 100644
index 990d3e0c1..000000000
--- a/plugwise/stick/connections/socket.py
+++ /dev/null
@@ -1,88 +0,0 @@
-"""Socket connection."""
-import logging
-import socket
-
-from ..connections import StickConnection
-from ..exceptions import PortError
-
-_LOGGER = logging.getLogger(__name__)
-
-
-class SocketConnection(StickConnection):
-    """Wrapper for Socket connection configuration."""
-
-    def __init__(self, port, parser):
-        super().__init__(port, parser)
-        # get the address from a <host>:<port> format
-        port_split = self.port.split(":")
-        self._socket_host = port_split[0]
-        self._socket_port = int(port_split[1])
-        self._socket_address = (self._socket_host, self._socket_port)
-
-        self._socket = None
-
-    def _open_connection(self):
-        """Open socket."""
-        _LOGGER.debug(
-            "Open socket to host '%s' at port %s",
-            self._socket_host,
-            str(self._socket_port),
-        )
-        try:
-            self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-            self._socket.connect(self._socket_address)
-        except Exception as err:
-            _LOGGER.debug(
-                "Failed to connect to host %s at port %s, %s",
-                self._socket_host,
-                str(self._socket_port),
-                err,
-            )
-            raise PortError(err)
-        self._reader_start("socket_reader_thread")
-        self._writer_start("socket_writer_thread")
-        self._is_connected = True
-        _LOGGER.debug(
-            "Successfully connected to host '%s' at port %s",
-            self._socket_host,
-            str(self._socket_port),
-        )
-
-    def _close_connection(self):
-        """Close the socket."""
-        try:
-            self._socket.close()
-        except Exception as err:
-            _LOGGER.debug(
-                "Failed to close socket to host %s at port %s, %s",
-                self._socket_host,
-                str(self._socket_port),
-                err,
-            )
-            raise PortError(err)
-
-    def _read_data(self):
-        """Read data from socket."""
-        if self._is_connected:
-            try:
-                socket_data = self._socket.recv(9999)
-            except Exception as err:
-                _LOGGER.debug(
-                    "Error while reading data from host %s at port %s : %s",
-                    self._socket_host,
-                    str(self._socket_port),
-                    err,
-                )
-                self._is_connected = False
-                raise PortError(err)
-            return socket_data
-        return None
-
-    def _write_data(self, data):
-        """Write data to socket."""
-        try:
-            self._socket.send(data)
-        except Exception as err:
-            _LOGGER.debug("Error while writing data to socket port : %s", err)
-            self._is_connected = False
-            raise PortError(err)
diff --git a/plugwise/stick/constants.py b/plugwise/stick/constants.py
deleted file mode 100644
index 04634a7fc..000000000
--- a/plugwise/stick/constants.py
+++ /dev/null
@@ -1,355 +0,0 @@
-"""Plugwise Stick (usb) constants."""
-from __future__ import annotations
-
-import datetime as dt
-import logging
-from typing import Final
-
-LOGGER = logging.getLogger(__name__)
-
-# Copied homeassistant.consts
-ARBITRARY_DATE: Final = dt.datetime(2022, 5, 14)
-ATTR_NAME: Final = "name"
-ATTR_STATE: Final = "state"
-ATTR_STATE_CLASS: Final = "state_class"
-ATTR_UNIT_OF_MEASUREMENT: Final = "unit_of_measurement"
-DEGREE: Final = "°"
-ELECTRIC_POTENTIAL_VOLT: Final = "V"
-ENERGY_KILO_WATT_HOUR: Final = "kWh"
-ENERGY_WATT_HOUR: Final = "Wh"
-PERCENTAGE: Final = "%"
-POWER_WATT: Final = "W"
-PRESET_AWAY: Final = "away"
-PRESSURE_BAR: Final = "bar"
-SIGNAL_STRENGTH_DECIBELS_MILLIWATT: Final = "dBm"
-TEMP_CELSIUS: Final = "°C"
-TEMP_KELVIN: Final = "°K"
-TIME_MILLISECONDS: Final = "ms"
-UNIT_LUMEN: Final = "lm"
-VOLUME_CUBIC_METERS: Final = "m³"
-VOLUME_CUBIC_METERS_PER_HOUR: Final = "m³/h"
-
-UTF8_DECODE: Final = "utf-8"
-SPECIAL_FORMAT: Final[tuple[str, ...]] = (ENERGY_KILO_WATT_HOUR, VOLUME_CUBIC_METERS)
-
-# Serial connection settings for plugwise USB stick
-BAUD_RATE: Final = 115200
-BYTE_SIZE: Final = 8
-PARITY: Final = "N"
-STOPBITS: Final = 1
-
-# Plugwise message identifiers
-MESSAGE_FOOTER: Final = b"\x0d\x0a"
-MESSAGE_HEADER: Final = b"\x05\x05\x03\x03"
-MESSAGE_LARGE: Final = "LARGE"
-MESSAGE_SMALL: Final = "SMALL"
-
-# Acknowledge message types
-
-# NodeAckSmallResponse
-RESPONSE_TYPE_SUCCESS: Final = b"00C1"
-RESPONSE_TYPE_ERROR: Final = b"00C2"
-RESPONSE_TYPE_TIMEOUT: Final = b"00E1"
-
-# NodeAckLargeResponse
-CLOCK_SET: Final = b"00D7"
-JOIN_REQUEST_ACCEPTED: Final = b"00D9"
-RELAY_SWITCHED_OFF: Final = b"00DE"
-RELAY_SWITCHED_ON: Final = b"00D8"
-RELAY_SWITCH_FAILED: Final = b"00E2"
-SLEEP_SET: Final = b"00F6"
-SLEEP_FAILED: Final = b"00F7"  # TODO: Validate
-REAL_TIME_CLOCK_ACCEPTED: Final = b"00DF"
-REAL_TIME_CLOCK_FAILED: Final = b"00E7"
-
-# NodeAckResponse
-SCAN_CONFIGURE_ACCEPTED: Final = b"00BE"
-SCAN_CONFIGURE_FAILED: Final = b"00BF"
-SCAN_LIGHT_CALIBRATION_ACCEPTED: Final = b"00BD"
-SENSE_INTERVAL_ACCEPTED: Final = b"00B3"
-SENSE_INTERVAL_FAILED: Final = b"00B4"
-SENSE_BOUNDARIES_ACCEPTED: Final = b"00B5"
-SENSE_BOUNDARIES_FAILED: Final = b"00B6"
-
-STATE_ACTIONS = (
-    RELAY_SWITCHED_ON,
-    RELAY_SWITCHED_OFF,
-    SCAN_CONFIGURE_ACCEPTED,
-    SLEEP_SET,
-)
-REQUEST_SUCCESS = (
-    CLOCK_SET,
-    JOIN_REQUEST_ACCEPTED,
-    REAL_TIME_CLOCK_ACCEPTED,
-    RELAY_SWITCHED_ON,
-    RELAY_SWITCHED_OFF,
-    SCAN_CONFIGURE_ACCEPTED,
-    SCAN_LIGHT_CALIBRATION_ACCEPTED,
-    SENSE_BOUNDARIES_ACCEPTED,
-    SENSE_INTERVAL_ACCEPTED,
-    SLEEP_SET,
-)
-REQUEST_FAILED = (
-    REAL_TIME_CLOCK_FAILED,
-    RELAY_SWITCH_FAILED,
-    RESPONSE_TYPE_ERROR,
-    RESPONSE_TYPE_TIMEOUT,
-    SCAN_CONFIGURE_FAILED,
-    SENSE_BOUNDARIES_FAILED,
-    SENSE_INTERVAL_FAILED,
-    SLEEP_FAILED,
-)
-STATUS_RESPONSES: Final[dict[bytes, str]] = {
-    # NodeAckSmallResponse
-    RESPONSE_TYPE_SUCCESS: "success",
-    RESPONSE_TYPE_ERROR: "error",
-    RESPONSE_TYPE_TIMEOUT: "timeout",
-    # NodeAckLargeResponse
-    CLOCK_SET: "clock set",
-    JOIN_REQUEST_ACCEPTED: "join accepted",
-    REAL_TIME_CLOCK_ACCEPTED: "real time clock set",
-    REAL_TIME_CLOCK_FAILED: "real time clock failed",
-    RELAY_SWITCHED_ON: "relay on",
-    RELAY_SWITCHED_OFF: "relay off",
-    RELAY_SWITCH_FAILED: "relay switching failed",
-    SLEEP_SET: "sleep settings accepted",
-    SLEEP_FAILED: "sleep settings failed",
-    # NodeAckResponse
-    SCAN_CONFIGURE_ACCEPTED: "Scan settings accepted",
-    SCAN_CONFIGURE_FAILED: "Scan settings failed",
-    SENSE_INTERVAL_ACCEPTED: "Sense report interval accepted",
-    SENSE_INTERVAL_FAILED: "Sense report interval failed",
-    SENSE_BOUNDARIES_ACCEPTED: "Sense boundaries accepted",
-    SENSE_BOUNDARIES_FAILED: "Sense boundaries failed",
-    SCAN_LIGHT_CALIBRATION_ACCEPTED: "Scan light calibration accepted",
-}
-
-# TODO: responses
-ACK_POWER_CALIBRATION: Final = b"00DA"
-ACK_CIRCLE_PLUS: Final = b"00DD"
-ACK_POWER_LOG_INTERVAL_SET: Final = b"00F8"
-
-# SED Awake status ID
-SED_AWAKE_MAINTENANCE: Final = 0  # SED awake for maintenance
-SED_AWAKE_FIRST: Final = 1  # SED awake for the first time
-SED_AWAKE_STARTUP: Final = (
-    2  # SED awake after restart, e.g. after reinserting a battery
-)
-SED_AWAKE_STATE: Final = 3  # SED awake to report state (Motion / Temperature / Humidity
-SED_AWAKE_UNKNOWN: Final = 4  # TODO: Unknown
-SED_AWAKE_BUTTON: Final = 5  # SED awake due to button press
-
-# Max timeout in seconds
-MESSAGE_TIME_OUT: Final = 15  # Stick responds with timeout messages after 10 sec.
-MESSAGE_RETRY: Final = 2
-
-# plugwise year information is offset from y2k
-PLUGWISE_EPOCH: Final = 2000
-PULSES_PER_KW_SECOND: Final = 468.9385193
-LOGADDR_OFFSET: Final = 278528
-
-# Default sleep between sending messages
-SLEEP_TIME: Final = 150 / 1000
-
-# Message priority levels
-PRIORITY_HIGH: Final = 1
-PRIORITY_LOW: Final = 3
-PRIORITY_MEDIUM: Final = 2
-
-# Max seconds the internal clock of plugwise nodes
-# are allowed to drift in seconds
-MAX_TIME_DRIFT: Final = 5
-
-# Default sleep time in seconds for watchdog daemon
-WATCHDOG_DEAMON: Final = 60
-
-# Automatically accept new join requests
-ACCEPT_JOIN_REQUESTS = False
-
-# Node types
-NODE_TYPE_STICK: Final = 0
-NODE_TYPE_CIRCLE_PLUS: Final = 1  # AME_NC
-NODE_TYPE_CIRCLE: Final = 2  # AME_NR
-NODE_TYPE_SWITCH: Final = 3  # AME_SEDSwitch
-NODE_TYPE_SENSE: Final = 5  # AME_SEDSense
-NODE_TYPE_SCAN: Final = 6  # AME_SEDScan
-NODE_TYPE_CELSIUS_SED: Final = 7  # AME_CelsiusSED
-NODE_TYPE_CELSIUS_NR: Final = 8  # AME_CelsiusNR
-NODE_TYPE_STEALTH: Final = 9  # AME_STEALTH_ZE
-# 10 AME_MSPBOOTLOAD
-# 11 AME_STAR
-
-# Hardware models based
-HW_MODELS: Final[dict[str, str]] = {
-    "038500": "Stick",
-    "070085": "Stick",
-    "120002": "Stick Legrand",
-    "120041": "Circle+ Legrand type E",
-    "120000": "Circle+ Legrand type F",
-    "090000": "Circle+ type B",
-    "090007": "Circle+ type B",
-    "090088": "Circle+ type E",
-    "070073": "Circle+ type F",
-    "090048": "Circle+ type G",
-    "120049": "Stealth M+",
-    "090188": "Stealth+",
-    "120040": "Circle Legrand type E",
-    "120001": "Circle Legrand type F",
-    "090079": "Circle type B",
-    "090087": "Circle type E",
-    "070140": "Circle type F",
-    "090093": "Circle type G",
-    "100025": "Circle",
-    "120048": "Stealth M",
-    "120029": "Stealth Legrand",
-    "090011": "Stealth",
-    "001200": "Stealth",
-    "080007": "Scan",
-    "110028": "Scan Legrand",
-    "070030": "Sense",
-    "120006": "Sense Legrand",
-    "070051": "Switch",
-    "080029": "Switch",
-}
-
-# Defaults for SED's (Sleeping End Devices)
-SED_STAY_ACTIVE: Final = 10  # Time in seconds the SED keep itself awake to receive and respond to other messages
-SED_SLEEP_FOR: Final = 60  # Time in minutes the SED will sleep
-SED_MAINTENANCE_INTERVAL: Final = 1440  # 24 hours, Interval in minutes the SED will get awake and notify it's available for maintenance purposes
-SED_CLOCK_SYNC = True  # Enable or disable synchronizing clock
-SED_CLOCK_INTERVAL: Final = (
-    25200  # 7 days, duration in minutes the node synchronize its clock
-)
-
-
-# Scan motion Sensitivity levels
-SCAN_SENSITIVITY_HIGH: Final = "high"
-SCAN_SENSITIVITY_MEDIUM: Final = "medium"
-SCAN_SENSITIVITY_OFF: Final = "medium"
-
-# Defaults for Scan Devices
-SCAN_MOTION_RESET_TIMER: Final = 5  # Time in minutes the motion sensor should not sense motion to report "no motion" state
-SCAN_SENSITIVITY = SCAN_SENSITIVITY_MEDIUM  # Default sensitivity of the motion sensors
-SCAN_DAYLIGHT_MODE = False  # Light override
-
-# Sense calculations
-SENSE_HUMIDITY_MULTIPLIER: Final = 125
-SENSE_HUMIDITY_OFFSET: Final = 6
-SENSE_TEMPERATURE_MULTIPLIER: Final = 175.72
-SENSE_TEMPERATURE_OFFSET: Final = 46.85
-
-# Callback types
-CB_NEW_NODE: Final = "NEW_NODE"
-CB_JOIN_REQUEST: Final = "JOIN_REQUEST"
-
-# Stick device features
-FEATURE_AVAILABLE: Final[dict[str, str]] = {
-    "id": "available",
-    "name": "Available",
-    "state": "available",
-    "unit": "state",
-}
-FEATURE_ENERGY_CONSUMPTION_TODAY: Final[dict[str, str]] = {
-    "id": "energy_consumption_today",
-    "name": "Energy consumption today",
-    "state": "Energy_consumption_today",
-    "unit": ENERGY_KILO_WATT_HOUR,
-}
-FEATURE_HUMIDITY: Final[dict[str, str]] = {
-    "id": "humidity",
-    "name": "Humidity",
-    "state": "humidity",
-    "unit": "%",
-}
-FEATURE_MOTION: Final[dict[str, str]] = {
-    "id": "motion",
-    "name": "Motion",
-    "state": "motion",
-    "unit": "state",
-}
-FEATURE_PING: Final[dict[str, str]] = {
-    "id": "ping",
-    "name": "Ping roundtrip",
-    "state": "ping",
-    "unit": TIME_MILLISECONDS,
-}
-FEATURE_POWER_USE: Final[dict[str, str]] = {
-    "id": "power_1s",
-    "name": "Power usage",
-    "state": "current_power_usage",
-    "unit": POWER_WATT,
-}
-FEATURE_POWER_USE_LAST_8_SEC: Final[dict[str, str]] = {
-    "id": "power_8s",
-    "name": "Power usage 8 seconds",
-    "state": "current_power_usage_8_sec",
-    "unit": POWER_WATT,
-}
-FEATURE_POWER_CONSUMPTION_CURRENT_HOUR: Final[dict[str, str]] = {
-    "id": "power_con_cur_hour",
-    "name": "Power consumption current hour",
-    "state": "power_consumption_current_hour",
-    "unit": ENERGY_KILO_WATT_HOUR,
-}
-FEATURE_POWER_CONSUMPTION_PREVIOUS_HOUR: Final[dict[str, str]] = {
-    "id": "power_con_prev_hour",
-    "name": "Power consumption previous hour",
-    "state": "power_consumption_previous_hour",
-    "unit": ENERGY_KILO_WATT_HOUR,
-}
-FEATURE_POWER_CONSUMPTION_TODAY: Final[dict[str, str]] = {
-    "id": "power_con_today",
-    "name": "Power consumption today",
-    "state": "power_consumption_today",
-    "unit": ENERGY_KILO_WATT_HOUR,
-}
-FEATURE_POWER_CONSUMPTION_YESTERDAY: Final[dict[str, str]] = {
-    "id": "power_con_yesterday",
-    "name": "Power consumption yesterday",
-    "state": "power_consumption_yesterday",
-    "unit": ENERGY_KILO_WATT_HOUR,
-}
-FEATURE_POWER_PRODUCTION_CURRENT_HOUR: Final[dict[str, str]] = {
-    "id": "power_prod_cur_hour",
-    "name": "Power production current hour",
-    "state": "power_production_current_hour",
-    "unit": ENERGY_KILO_WATT_HOUR,
-}
-FEATURE_POWER_PRODUCTION_PREVIOUS_HOUR: Final[dict[str, str]] = {
-    "id": "power_prod_prev_hour",
-    "name": "Power production previous hour",
-    "state": "power_production_previous_hour",
-    "unit": ENERGY_KILO_WATT_HOUR,
-}
-FEATURE_RELAY: Final[dict[str, str]] = {
-    "id": "relay",
-    "name": "Relay state",
-    "state": "relay_state",
-    "unit": "state",
-}
-FEATURE_SWITCH: Final[dict[str, str]] = {
-    "id": "switch",
-    "name": "Switch state",
-    "state": "switch_state",
-    "unit": "state",
-}
-FEATURE_TEMPERATURE: Final[dict[str, str]] = {
-    "id": "temperature",
-    "name": "Temperature",
-    "state": "temperature",
-    "unit": TEMP_CELSIUS,
-}
-
-# TODO: Need to validate RSSI sensors
-FEATURE_RSSI_IN: Final[dict[str, str]] = {
-    "id": "RSSI_in",
-    "name": "RSSI in",
-    "state": "rssi_in",
-    "unit": "Unknown",
-}
-FEATURE_RSSI_OUT: Final[dict[str, str]] = {
-    "id": "RSSI_out",
-    "name": "RSSI out",
-    "state": "rssi_out",
-    "unit": "Unknown",
-}
diff --git a/plugwise/stick/controller.py b/plugwise/stick/controller.py
deleted file mode 100644
index b00ecad48..000000000
--- a/plugwise/stick/controller.py
+++ /dev/null
@@ -1,441 +0,0 @@
-"""
-Message controller for USB-Stick
-
-The controller will:
-- handle the connection (connect/disconnect) to the USB-Stick
-- take care for message acknowledgements based on sequence id's
-- resend message requests when timeouts occurs
-- holds a sending queue and submit messages based on the message priority (high, medium, low)
-- passes received messages back to message processor (stick.py)
-- execution of callbacks after processing the response message
-
-"""
-
-from datetime import datetime, timedelta
-import logging
-from queue import Empty, SimpleQueue
-import threading
-import time
-
-from .connections.serial import PlugwiseUSBConnection
-from .connections.socket import SocketConnection
-from .constants import (
-    MESSAGE_RETRY,
-    MESSAGE_TIME_OUT,
-    PRIORITY_MEDIUM,
-    REQUEST_FAILED,
-    REQUEST_SUCCESS,
-    SLEEP_TIME,
-    STATUS_RESPONSES,
-    UTF8_DECODE,
-)
-from .messages.requests import NodeInfoRequest, NodePingRequest, NodeRequest
-from .messages.responses import (
-    NodeAckLargeResponse,
-    NodeAckResponse,
-    NodeAckSmallResponse,
-)
-from .parser import PlugwiseParser
-from .util import inc_seq_id
-
-_LOGGER = logging.getLogger(__name__)
-
-
-class StickMessageController:
-    """Handle connection and message sending and receiving"""
-
-    def __init__(self, port: str, message_processor, node_state):
-        """Initialize message controller"""
-        self.connection = None
-        self.discovery_finished = False
-        self.expected_responses = {}
-        self.lock_expected_responses = threading.Lock()
-        self.init_callback = None
-        self.last_seq_id = None
-        self.message_processor = message_processor
-        self.node_state = node_state
-        self.parser = PlugwiseParser(self.message_handler)
-        self.port = port
-
-        self._send_message_queue = None
-        self._send_message_thread = None
-        self._receive_timeout_thread = False
-        self._receive_timeout_thread_state = False
-        self._send_message_thread_state = False
-
-    @property
-    def receive_timeout_thread_state(self) -> bool:
-        """Required state of the receive timeout thread"""
-        return self._receive_timeout_thread_state
-
-    @property
-    def receive_timeout_thread_is_alive(self) -> bool:
-        """Current state of the receive timeout thread"""
-        return self._send_message_thread.is_alive()
-
-    @property
-    def send_message_thread_state(self) -> bool:
-        """Required state of the send message thread"""
-        return self._send_message_thread_state
-
-    @property
-    def send_message_thread_is_alive(self) -> bool:
-        """Current state of the send message thread"""
-        return self._send_message_thread.is_alive()
-
-    def connect_to_stick(self, callback=None) -> bool:
-        """
-        Connect to USB-Stick and startup all worker threads
-
-        Return: True when connection is successful.
-        """
-        self.init_callback = callback
-        # Open connection to USB Stick
-        if ":" in self.port:
-            _LOGGER.debug(
-                "Open socket connection to %s hosting Plugwise USB stick", self.port
-            )
-            self.connection = SocketConnection(self.port, self.parser.feed)
-        else:
-            _LOGGER.debug("Open USB serial connection to Plugwise USB stick")
-            self.connection = PlugwiseUSBConnection(self.port, self.parser.feed)
-        if self.connection.connect():
-            _LOGGER.debug("Starting message controller threads...")
-            # send daemon
-            self._send_message_queue = SimpleQueue()
-            self._send_message_thread_state = True
-            self._send_message_thread = threading.Thread(
-                None, self._send_message_loop, "send_messages_thread", (), {}
-            )
-            self._send_message_thread.daemon = True
-            self._send_message_thread.start()
-            # receive timeout daemon
-            self._receive_timeout_thread_state = True
-            self._receive_timeout_thread = threading.Thread(
-                None, self._receive_timeout_loop, "receive_timeout_thread", (), {}
-            )
-            self._receive_timeout_thread.daemon = True
-            self._receive_timeout_thread.start()
-            _LOGGER.debug("All message controller threads started")
-        else:
-            _LOGGER.warning("Failed to connect to USB stick")
-        return self.connection.is_connected()
-
-    def send(
-        self,
-        request: NodeRequest,
-        callback=None,
-        retry_counter=0,
-        priority=PRIORITY_MEDIUM,
-    ):
-        """Queue request message to be sent into Plugwise Zigbee network."""
-        _LOGGER.debug(
-            "Queue %s to be send with retry counter %s and priority %s",
-            request.__class__.__name__,
-            str(retry_counter),
-            str(priority),
-        )
-        self._send_message_queue.put(
-            (
-                priority,
-                retry_counter,
-                datetime.now(),
-                [
-                    request,
-                    callback,
-                    retry_counter,
-                    None,
-                ],
-            )
-        )
-
-    def resend(self, seq_id):
-        """Resend message."""
-        _mac = "<unknown>"
-        with self.lock_expected_responses:
-            if not self.expected_responses.get(seq_id):
-                _LOGGER.warning(
-                    "Cannot resend unknown request %s",
-                    str(seq_id),
-                )
-            else:
-                if self.expected_responses[seq_id][0].mac:
-                    _mac = self.expected_responses[seq_id][0].mac.decode(UTF8_DECODE)
-                _request = self.expected_responses[seq_id][0].__class__.__name__
-
-                if self.expected_responses[seq_id][2] == -1:
-                    _LOGGER.debug("Drop single %s to %s ", _request, _mac)
-                elif self.expected_responses[seq_id][2] <= MESSAGE_RETRY:
-                    if (
-                        isinstance(self.expected_responses[seq_id][0], NodeInfoRequest)
-                        and not self.discovery_finished
-                    ):
-                        # Time out for node which is not discovered yet
-                        # to speedup the initial discover phase skip retries and mark node as not discovered.
-                        _LOGGER.debug(
-                            "Skip retry %s to %s to speedup discover process",
-                            _request,
-                            _mac,
-                        )
-                        if self.expected_responses[seq_id][1]:
-                            self.expected_responses[seq_id][1]()
-                    else:
-                        _LOGGER.info(
-                            "Resend %s for %s, retry %s of %s",
-                            _request,
-                            _mac,
-                            str(self.expected_responses[seq_id][2] + 1),
-                            str(MESSAGE_RETRY + 1),
-                        )
-                        self.send(
-                            self.expected_responses[seq_id][0],
-                            self.expected_responses[seq_id][1],
-                            self.expected_responses[seq_id][2] + 1,
-                        )
-                else:
-                    _LOGGER.warning(
-                        "Drop %s to %s because max retries %s reached",
-                        _request,
-                        _mac,
-                        str(MESSAGE_RETRY + 1),
-                    )
-                    # Report node as unavailable for missing NodePingRequest
-                    if isinstance(self.expected_responses[seq_id][0], NodePingRequest):
-                        self.node_state(_mac, False)
-                    else:
-                        _LOGGER.debug(
-                            "Do a single ping request to %s to validate if node is reachable",
-                            _mac,
-                        )
-                        self.send(
-                            NodePingRequest(self.expected_responses[seq_id][0].mac),
-                            None,
-                            MESSAGE_RETRY + 1,
-                        )
-                del self.expected_responses[seq_id]
-
-    def _send_message_loop(self):
-        """Daemon to send messages waiting in queue."""
-        while self._send_message_thread_state:
-            try:
-                _prio, _retry, _dt, request_set = self._send_message_queue.get(
-                    block=True, timeout=1
-                )
-            except Empty:
-                time.sleep(SLEEP_TIME)
-            else:
-                # Calc next seq_id based last received ack message
-                # if previous seq_id is unknown use fake b"0000"
-                seq_id = inc_seq_id(self.last_seq_id)
-                with self.lock_expected_responses:
-                    self.expected_responses[seq_id] = request_set
-                    if self.expected_responses[seq_id][2] == 0:
-                        _LOGGER.info(
-                            "Send %s to %s using seq_id %s",
-                            self.expected_responses[seq_id][0].__class__.__name__,
-                            self.expected_responses[seq_id][0].mac,
-                            str(seq_id),
-                        )
-                    else:
-                        _LOGGER.info(
-                            "Resend %s to %s using seq_id %s, retry %s",
-                            self.expected_responses[seq_id][0].__class__.__name__,
-                            self.expected_responses[seq_id][0].mac,
-                            str(seq_id),
-                            str(self.expected_responses[seq_id][2]),
-                        )
-                    self.expected_responses[seq_id][3] = datetime.now()
-                    # Send request
-                    self.connection.send(self.expected_responses[seq_id][0])
-                time.sleep(SLEEP_TIME)
-                timeout_counter = 0
-                # Wait max 1 second for acknowledge response from USB-stick
-                while (
-                    self.last_seq_id != seq_id
-                    and timeout_counter < 10
-                    and seq_id != b"0000"
-                    and self.last_seq_id is not None
-                ):
-                    time.sleep(0.1)
-                    timeout_counter += 1
-                if timeout_counter >= 10 and self._send_message_thread_state:
-                    self.resend(seq_id)
-        _LOGGER.debug("Send message loop stopped")
-
-    def message_handler(self, message):
-        """handle received message from Plugwise Zigbee network."""
-
-        # only save last seq_id and skip special ID's FFFD, FFFE, FFFF
-        if self.last_seq_id:
-            if int(self.last_seq_id, 16) < int(message.seq_id, 16) < 65533:
-                self.last_seq_id = message.seq_id
-            elif message.seq_id == b"0000" and self.last_seq_id == b"FFFB":
-                self.last_seq_id = b"0000"
-
-        if isinstance(message, NodeAckSmallResponse):
-            self._log_status_message(message, message.ack_id)
-            self._post_message_action(
-                message.seq_id, message.ack_id, message.__class__.__name__
-            )
-        else:
-            if isinstance(message, (NodeAckResponse, NodeAckLargeResponse)):
-                self._log_status_message(message, message.ack_id)
-            else:
-                self._log_status_message(message)
-            self.message_processor(message)
-            if message.seq_id not in [b"FFFF", b"FFFE", b"FFFD"]:
-                self._post_message_action(
-                    message.seq_id, None, message.__class__.__name__
-                )
-
-    def _post_message_action(self, seq_id, ack_response=None, request="unknown"):
-        """Execute action if request has been successful."""
-        resend_request = False
-        with self.lock_expected_responses:
-            if seq_id in self.expected_responses:
-                if ack_response in (*REQUEST_SUCCESS, None):
-                    if self.expected_responses[seq_id][1]:
-                        _LOGGER.debug(
-                            "Execute action %s of request with seq_id %s",
-                            self.expected_responses[seq_id][1].__name__,
-                            str(seq_id),
-                        )
-                        try:
-                            self.expected_responses[seq_id][1]()
-                        # TODO: narrow exception
-                        except Exception as err:  # pylint: disable=broad-except
-                            _LOGGER.error(
-                                "Execution of  %s for request with seq_id %s failed: %s",
-                                self.expected_responses[seq_id][1].__name__,
-                                str(seq_id),
-                                err,
-                            )
-                    del self.expected_responses[seq_id]
-                elif ack_response in REQUEST_FAILED:
-                    resend_request = True
-            else:
-                if not self.last_seq_id:
-                    if b"0000" in self.expected_responses:
-                        self.expected_responses[seq_id] = self.expected_responses[
-                            b"0000"
-                        ]
-                        del self.expected_responses[b"0000"]
-                    self.last_seq_id = seq_id
-                else:
-                    _LOGGER.info(
-                        "Drop unexpected %s%s using seq_id %s",
-                        STATUS_RESPONSES.get(ack_response, "") + " ",
-                        request,
-                        str(seq_id),
-                    )
-        if resend_request:
-            self.resend(seq_id)
-
-    def _receive_timeout_loop(self):
-        """Daemon to time out open requests without any (n)ack response message."""
-        while self._receive_timeout_thread_state:
-            resend_list = []
-            with self.lock_expected_responses:
-                for seq_id in list(self.expected_responses.keys()):
-                    if self.expected_responses[seq_id][3] is not None:
-                        if self.expected_responses[seq_id][3] < (
-                            datetime.now() - timedelta(seconds=MESSAGE_TIME_OUT)
-                        ):
-                            _mac = "<unknown>"
-                            if self.expected_responses[seq_id][0].mac:
-                                _mac = self.expected_responses[seq_id][0].mac
-                            _LOGGER.info(
-                                "No response within %s seconds timeout for %s to %s with sequence ID %s",
-                                str(MESSAGE_TIME_OUT),
-                                self.expected_responses[seq_id][0].__class__.__name__,
-                                _mac,
-                                str(seq_id),
-                            )
-                            resend_list.append(seq_id)
-            for seq_id in resend_list:
-                self.resend(seq_id)
-            receive_timeout_checker = 0
-            while (
-                receive_timeout_checker < MESSAGE_TIME_OUT
-                and self._receive_timeout_thread_state
-            ):
-                time.sleep(1)
-                receive_timeout_checker += 1
-        _LOGGER.debug("Receive timeout loop stopped")
-
-    def _log_status_message(self, message, status=None):
-        """Log status messages.."""
-        if status:
-            if status in STATUS_RESPONSES:
-                _LOGGER.debug(
-                    "Received %s %s for request with seq_id %s",
-                    STATUS_RESPONSES[status],
-                    message.__class__.__name__,
-                    str(message.seq_id),
-                )
-            else:
-                with self.lock_expected_responses:
-                    if self.expected_responses.get(message.seq_id):
-                        _LOGGER.warning(
-                            "Received unmanaged (%s) %s in response to %s with seq_id %s",
-                            str(status),
-                            message.__class__.__name__,
-                            str(
-                                self.expected_responses[message.seq_id][
-                                    1
-                                ].__class__.__name__
-                            ),
-                            str(message.seq_id),
-                        )
-                    else:
-                        _LOGGER.warning(
-                            "Received unmanaged (%s) %s for unknown request with seq_id %s",
-                            str(status),
-                            message.__class__.__name__,
-                            str(message.seq_id),
-                        )
-        else:
-            _LOGGER.info(
-                "Received %s from %s with sequence id %s",
-                message.__class__.__name__,
-                message.mac.decode(UTF8_DECODE),
-                str(message.seq_id),
-            )
-
-    def disconnect_from_stick(self):
-        """Disconnect from stick and raise error if it fails"""
-        self._send_message_thread_state = False
-        self._receive_timeout_thread_state = False
-        self.connection.disconnect()
-
-    def restart_receive_timeout_thread(self):
-        """Restart the receive timeout thread if not running"""
-        if not self._receive_timeout_thread.is_alive():
-            _LOGGER.warning(
-                "Unexpected halt of receive thread, restart thread",
-            )
-            self._receive_timeout_thread = threading.Thread(
-                None,
-                self._receive_timeout_loop,
-                "receive_timeout_thread",
-                (),
-                {},
-            )
-            self._receive_timeout_thread.daemon = True
-            self._receive_timeout_thread.start()
-
-    def restart_send_message_thread(self):
-        """Restart the message sender thread if not running"""
-        if not self._send_message_thread.is_alive():
-            _LOGGER.warning(
-                "Unexpected halt of send thread, restart thread",
-            )
-            self._send_message_thread = threading.Thread(
-                None,
-                self._send_message_loop,
-                "send_messages_thread",
-                (),
-                {},
-            )
-            self._send_message_thread.daemon = True
-            self._send_message_thread.start()
diff --git a/plugwise/stick/exceptions.py b/plugwise/stick/exceptions.py
deleted file mode 100644
index c18b1a699..000000000
--- a/plugwise/stick/exceptions.py
+++ /dev/null
@@ -1,41 +0,0 @@
-"""Plugwise Stick Exceptions."""
-
-
-class PlugwiseException(Exception):
-    """Base error class for this Plugwise library."""
-
-
-class PortError(PlugwiseException):
-    """Connection to USBstick failed."""
-
-
-class StickInitError(PlugwiseException):
-    """Initialization of USBstick failed."""
-
-
-class NetworkDown(PlugwiseException):
-    """Zigbee network not online."""
-
-
-class CirclePlusError(PlugwiseException):
-    """Connection to Circle+ node failed."""
-
-
-class InvalidMessageLength(PlugwiseException):
-    """Invalid message length."""
-
-
-class InvalidMessageHeader(PlugwiseException):
-    """Invalid message header."""
-
-
-class InvalidMessageFooter(PlugwiseException):
-    """Invalid message footer."""
-
-
-class InvalidMessageChecksum(PlugwiseException):
-    """Invalid data checksum."""
-
-
-class TimeoutException(PlugwiseException):
-    """Timeout expired while waiting for response from node."""
diff --git a/plugwise/stick/messages/__init__.py b/plugwise/stick/messages/__init__.py
deleted file mode 100644
index 4f8d28efb..000000000
--- a/plugwise/stick/messages/__init__.py
+++ /dev/null
@@ -1,30 +0,0 @@
-"""Plugwise messages."""
-
-from ..constants import MESSAGE_FOOTER, MESSAGE_HEADER, UTF8_DECODE
-from ..util import crc_fun
-
-
-class PlugwiseMessage:
-    """Plugwise message base."""
-
-    ID = b"0000"
-
-    def __init__(self):
-        self.mac = ""
-        self.checksum = None
-        self.args = []
-
-    def serialize(self):
-        """Return message in a serialized format that can be sent out on wire."""
-        _args = b"".join(a.serialize() for a in self.args)
-        msg = self.ID
-        if self.mac != "":
-            msg += self.mac
-        msg += _args
-        self.checksum = self.calculate_checksum(msg)
-        return MESSAGE_HEADER + msg + self.checksum + MESSAGE_FOOTER
-
-    @staticmethod
-    def calculate_checksum(something):
-        """Calculate crc checksum."""
-        return bytes("%04X" % crc_fun(something), UTF8_DECODE)
diff --git a/plugwise/stick/messages/requests.py b/plugwise/stick/messages/requests.py
deleted file mode 100644
index a20ac0be1..000000000
--- a/plugwise/stick/messages/requests.py
+++ /dev/null
@@ -1,529 +0,0 @@
-"""All known request messages to be send to plugwise devices."""
-from ..constants import MESSAGE_FOOTER, MESSAGE_HEADER
-from ..messages import PlugwiseMessage
-from ..util import (
-    DateTime,
-    Int,
-    LogAddr,
-    RealClockDate,
-    RealClockTime,
-    SInt,
-    String,
-    Time,
-)
-
-
-class NodeRequest(PlugwiseMessage):
-    """Base class for request messages to be send from by USB-Stick."""
-
-    def __init__(self, mac):
-        PlugwiseMessage.__init__(self)
-        self.args = []
-        self.mac = mac
-
-
-class NodeNetworkInfoRequest(NodeRequest):
-    """TODO: PublicNetworkInfoRequest
-
-    No arguments
-    """
-
-    ID = b"0001"
-
-
-class CirclePlusConnectRequest(NodeRequest):
-    """
-    Request to connect a Circle+ to the Stick
-
-    Response message: CirclePlusConnectResponse
-    """
-
-    ID = b"0004"
-
-    # This message has an exceptional format and therefore need to override the serialize method
-    def serialize(self):
-        # This command has args: byte: key, byte: networkinfo.index, ulong: networkkey = 0
-        args = b"00000000000000000000"
-        msg = self.ID + args + self.mac
-        checksum = self.calculate_checksum(msg)
-        return MESSAGE_HEADER + msg + checksum + MESSAGE_FOOTER
-
-
-class NodeAddRequest(NodeRequest):
-    """
-    Inform node it is added to the Plugwise Network it to memory of Circle+ node
-
-    Response message: [acknowledge message]
-    """
-
-    ID = b"0007"
-
-    def __init__(self, mac, accept: bool):
-        super().__init__(mac)
-        accept_value = 1 if accept else 0
-        self.args.append(Int(accept_value, length=2))
-
-    # This message has an exceptional format (MAC at end of message)
-    # and therefore a need to override the serialize method
-    def serialize(self):
-        args = b"".join(a.serialize() for a in self.args)
-        msg = self.ID + args + self.mac
-        checksum = self.calculate_checksum(msg)
-        return MESSAGE_HEADER + msg + checksum + MESSAGE_FOOTER
-
-
-class NodeAllowJoiningRequest(NodeRequest):
-    """
-    Enable or disable receiving joining request of unjoined nodes.
-    Circle+ node will respond with an acknowledge message
-
-    Response message: NodeAckLargeResponse
-    """
-
-    ID = b"0008"
-
-    def __init__(self, accept: bool):
-        super().__init__("")
-        # TODO: Make sure that '01' means enable, and '00' disable joining
-        val = 1 if accept else 0
-        self.args.append(Int(val, length=2))
-
-
-class NodeResetRequest(NodeRequest):
-    """
-    TODO: Some kind of reset request
-
-    Response message: ???
-    """
-
-    ID = b"0009"
-
-    def __init__(self, mac, moduletype, timeout):
-        super().__init__(mac)
-        self.args += [
-            Int(moduletype, length=2),
-            Int(timeout, length=2),
-        ]
-
-
-class StickInitRequest(NodeRequest):
-    """
-    Initialize USB-Stick
-
-    Response message: StickInitResponse
-    """
-
-    ID = b"000A"
-
-    def __init__(self):
-        """message for that initializes the Stick"""
-        # init is the only request message that doesn't send MAC address
-        super().__init__("")
-
-
-class NodeImagePrepareRequest(NodeRequest):
-    """
-    TODO: PWEswImagePrepareRequestV1_0
-
-    Response message: TODO:
-    """
-
-    ID = b"000B"
-
-
-class NodePingRequest(NodeRequest):
-    """
-    Ping node
-
-    Response message: NodePingResponse
-    """
-
-    ID = b"000D"
-
-
-class CirclePowerUsageRequest(NodeRequest):
-    """
-    Request current power usage
-
-    Response message: CirclePowerUsageResponse
-    """
-
-    ID = b"0012"
-
-
-class CircleClockSetRequest(NodeRequest):
-    """
-    Set internal clock of node
-
-    Response message: [Acknowledge message]
-    """
-
-    ID = b"0016"
-
-    def __init__(self, mac, dt):
-        super().__init__(mac)
-        passed_days = dt.day - 1
-        month_minutes = (passed_days * 24 * 60) + (dt.hour * 60) + dt.minute
-        this_date = DateTime(dt.year, dt.month, month_minutes)
-        this_time = Time(dt.hour, dt.minute, dt.second)
-        day_of_week = Int(dt.weekday(), 2)
-        # FIXME: use LogAddr instead
-        log_buf_addr = String("FFFFFFFF", 8)
-        self.args += [this_date, log_buf_addr, this_time, day_of_week]
-
-
-class CircleSwitchRelayRequest(NodeRequest):
-    """
-    switches relay on/off
-
-    Response message: NodeAckLargeResponse
-    """
-
-    ID = b"0017"
-
-    def __init__(self, mac, on):
-        super().__init__(mac)
-        val = 1 if on else 0
-        self.args.append(Int(val, length=2))
-
-
-class CirclePlusScanRequest(NodeRequest):
-    """
-    Get all linked Circle plugs from Circle+
-    a Plugwise network can have 64 devices the node ID value has a range from 0 to 63
-
-    Response message: CirclePlusScanResponse
-    """
-
-    ID = b"0018"
-
-    def __init__(self, mac, node_address):
-        super().__init__(mac)
-        self.args.append(Int(node_address, length=2))
-        self.node_address = node_address
-
-
-class NodeRemoveRequest(NodeRequest):
-    """
-    Request node to be removed from Plugwise network by
-    removing it from memory of Circle+ node.
-
-    Response message: NodeRemoveResponse
-    """
-
-    ID = b"001C"
-
-    def __init__(self, mac_circle_plus, mac_to_unjoined):
-        super().__init__(mac_circle_plus)
-        self.args.append(String(mac_to_unjoined, length=16))
-
-
-class NodeInfoRequest(NodeRequest):
-    """
-    Request status info of node
-
-    Response message: NodeInfoResponse
-    """
-
-    ID = b"0023"
-
-
-class CircleCalibrationRequest(NodeRequest):
-    """
-    Request power calibration settings of node
-
-    Response message: CircleCalibrationResponse
-    """
-
-    ID = b"0026"
-
-
-class CirclePlusRealTimeClockSetRequest(NodeRequest):
-    """
-    Set real time clock of CirclePlus
-
-    Response message: [Acknowledge message]
-    """
-
-    ID = b"0028"
-
-    def __init__(self, mac, dt):
-        super().__init__(mac)
-        this_time = RealClockTime(dt.hour, dt.minute, dt.second)
-        day_of_week = Int(dt.weekday(), 2)
-        this_date = RealClockDate(dt.day, dt.month, dt.year)
-        self.args += [this_time, day_of_week, this_date]
-
-
-class CirclePlusRealTimeClockGetRequest(NodeRequest):
-    """
-    Request current real time clock of CirclePlus
-
-    Response message: CirclePlusRealTimeClockResponse
-    """
-
-    ID = b"0029"
-
-
-class CircleClockGetRequest(NodeRequest):
-    """
-    Request current internal clock of node
-
-    Response message: CircleClockResponse
-    """
-
-    ID = b"003E"
-
-
-class CircleEnableScheduleRequest(NodeRequest):
-    """
-    Request to switch Schedule on or off
-
-    Response message: TODO:
-    """
-
-    ID = b"0040"
-
-    def __init__(self, mac, on):
-        super().__init__(mac)
-        val = 1 if on else 0
-        self.args.append(Int(val, length=2))
-        # the second parameter is always 0x01
-        self.args.append(Int(1, length=2))
-
-
-class NodeAddToGroupRequest(NodeRequest):
-    """
-    Add node to group
-
-    Response message: TODO:
-    """
-
-    ID = b"0045"
-
-    def __init__(self, mac, group_mac, task_id, port_mask):
-        super().__init__(mac)
-        group_mac_val = String(group_mac, length=16)
-        task_id_val = String(task_id, length=16)
-        port_mask_val = String(port_mask, length=16)
-        self.args += [group_mac_val, task_id_val, port_mask_val]
-
-
-class NodeRemoveFromGroupRequest(NodeRequest):
-    """
-    Remove node from group
-
-    Response message: TODO:
-    """
-
-    ID = b"0046"
-
-    def __init__(self, mac, group_mac):
-        super().__init__(mac)
-        group_mac_val = String(group_mac, length=16)
-        self.args += [group_mac_val]
-
-
-class NodeBroadcastGroupSwitchRequest(NodeRequest):
-    """
-    Broadcast to group to switch
-
-    Response message: TODO:
-    """
-
-    ID = b"0047"
-
-    def __init__(self, group_mac, switch_state: bool):
-        super().__init__(group_mac)
-        val = 1 if switch_state else 0
-        self.args.append(Int(val, length=2))
-
-
-class CircleEnergyCountersRequest(NodeRequest):
-    """
-    Request energy usage counters storaged a given memory address
-
-    Response message: CircleEnergyCountersResponse
-    """
-
-    ID = b"0048"
-
-    def __init__(self, mac, log_address):
-        super().__init__(mac)
-        self.args.append(LogAddr(log_address, 8))
-
-
-class NodeSleepConfigRequest(NodeRequest):
-    """
-    Configure timers for SED nodes to minimize battery usage
-
-    stay_active             : Duration in seconds the SED will be awake for receiving commands
-    sleep_for               : Duration in minutes the SED will be in sleeping mode and not able to respond any command
-    maintenance_interval    : Interval in minutes the node will wake up and able to receive commands
-    clock_sync              : Enable/disable clock sync
-    clock_interval          : Duration in minutes the node synchronize its clock
-
-    Response message: Ack message with SLEEP_SET
-    """
-
-    ID = b"0050"
-
-    def __init__(
-        self,
-        mac,
-        stay_active: int,
-        maintenance_interval: int,
-        sleep_for: int,
-        sync_clock: bool,
-        clock_interval: int,
-    ):
-        super().__init__(mac)
-
-        stay_active_val = Int(stay_active, length=2)
-        sleep_for_val = Int(sleep_for, length=4)
-        maintenance_interval_val = Int(maintenance_interval, length=4)
-        val = 1 if sync_clock else 0
-        clock_sync_val = Int(val, length=2)
-        clock_interval_val = Int(clock_interval, length=4)
-        self.args += [
-            stay_active_val,
-            maintenance_interval_val,
-            sleep_for_val,
-            clock_sync_val,
-            clock_interval_val,
-        ]
-
-
-class NodeSelfRemoveRequest(NodeRequest):
-    """
-    <command number="0051" vnumber="1.0" implementation="Plugwise.IO.Commands.V20.PWSelfRemovalRequestV1_0">
-      <arguments>
-        <argument name="macId" length="16"/>
-      </arguments>
-    </command>
-
-    """
-
-    ID = b"0051"
-
-
-class NodeMeasureIntervalRequest(NodeRequest):
-    """
-    Configure the logging interval of power measurement in minutes
-
-    Response message: TODO:
-    """
-
-    ID = b"0057"
-
-    def __init__(self, mac, usage, production):
-        super().__init__(mac)
-        self.args.append(Int(usage, length=4))
-        self.args.append(Int(production, length=4))
-
-
-class NodeClearGroupMacRequest(NodeRequest):
-    """
-    TODO:
-
-    Response message: ????
-    """
-
-    ID = b"0058"
-
-    def __init__(self, mac, taskId):
-        super().__init__(mac)
-        self.args.append(Int(taskId, length=2))
-
-
-class CircleSetScheduleValueRequest(NodeRequest):
-    """
-    Send chunk of On/Off/StandbyKiller Schedule to Circle(+)
-
-    Response message: TODO:
-    """
-
-    ID = b"0059"
-
-    def __init__(self, mac, val):
-        super().__init__(mac)
-        self.args.append(SInt(val, length=4))
-
-
-class NodeFeaturesRequest(NodeRequest):
-    """
-    Request feature set node supports
-
-    Response message: NodeFeaturesResponse
-    """
-
-    ID = b"005F"
-
-
-class ScanConfigureRequest(NodeRequest):
-    """
-    Configure a Scan node
-
-    reset_timer : Delay in minutes when signal is send when no motion is detected
-    sensitivity : Sensitivity of Motion sensor (High, Medium, Off)
-    light       : Daylight override to only report motion when lightlevel is below calibrated level
-
-    Response message: [Acknowledge message]
-    """
-
-    ID = b"0101"
-
-    def __init__(self, mac, reset_timer: int, sensitivity: int, light: bool):
-        super().__init__(mac)
-
-        reset_timer_value = Int(reset_timer, length=2)
-        # Sensitivity: HIGH(0x14),  MEDIUM(0x1E),  OFF(0xFF)
-        sensitivity_value = Int(sensitivity, length=2)
-        light_temp = 1 if light else 0
-        light_value = Int(light_temp, length=2)
-        self.args += [
-            sensitivity_value,
-            light_value,
-            reset_timer_value,
-        ]
-
-
-class ScanLightCalibrateRequest(NodeRequest):
-    """
-    Calibrate light sensitivity
-
-    Response message: [Acknowledge message]
-    """
-
-    ID = b"0102"
-
-
-class SenseReportIntervalRequest(NodeRequest):
-    """
-    Sets the Sense temperature and humidity measurement report interval in minutes.
-    Based on this interval, periodically a 'SenseReportResponse' message is sent by the Sense node
-
-    Response message: [Acknowledge message]
-    """
-
-    ID = b"0102"
-
-    def __init__(self, mac, interval):
-        super().__init__(mac)
-        self.args.append(Int(interval, length=2))
-
-
-class CircleInitialRelaisStateRequest(NodeRequest):
-    """
-    Get or set initial Relais state
-
-    Response message: CircleInitialRelaisStateResponse
-    """
-
-    ID = b"0138"
-
-    def __init__(self, mac, configure: bool, relais_state: bool):
-        super().__init__(mac)
-        set_or_get = Int(1 if configure else 0, length=2)
-        relais = Int(1 if relais_state else 0, length=2)
-        self.args += [set_or_get, relais]
diff --git a/plugwise/stick/messages/responses.py b/plugwise/stick/messages/responses.py
deleted file mode 100644
index 50380bd64..000000000
--- a/plugwise/stick/messages/responses.py
+++ /dev/null
@@ -1,607 +0,0 @@
-"""All known response messages to be received from plugwise devices."""
-from datetime import datetime
-
-from ..constants import MESSAGE_FOOTER, MESSAGE_HEADER, MESSAGE_LARGE, MESSAGE_SMALL
-from ..exceptions import (
-    InvalidMessageChecksum,
-    InvalidMessageFooter,
-    InvalidMessageHeader,
-    InvalidMessageLength,
-)
-from ..messages import PlugwiseMessage
-from ..util import (
-    DateTime,
-    Float,
-    Int,
-    LogAddr,
-    RealClockDate,
-    RealClockTime,
-    String,
-    Time,
-    UnixTimestamp,
-)
-
-
-class NodeResponse(PlugwiseMessage):
-    """
-    Base class for response messages received by USB-Stick.
-    """
-
-    def __init__(self, format_size=None):
-        super().__init__()
-        self.format_size = format_size
-        self.params = []
-        self.timestamp = None
-        self.seq_id = None
-        self.msg_id = None
-        self.ack_id = None
-        if self.format_size == MESSAGE_SMALL:
-            self.len_correction = -12
-        elif self.format_size == MESSAGE_LARGE:
-            self.len_correction = 4
-        else:
-            self.len_correction = 0
-
-    def deserialize(self, response):
-        self.timestamp = datetime.now()
-        if response[:4] != MESSAGE_HEADER:
-            raise InvalidMessageHeader(
-                f"Invalid message header {str(response[:4])} for {self.__class__.__name__}"
-            )
-        if response[-2:] != MESSAGE_FOOTER:
-            raise InvalidMessageFooter(
-                f"Invalid message footer {str(response[-2:])} for {self.__class__.__name__}"
-            )
-        _calculated_checksum = self.calculate_checksum(response[4:-6])
-        _message_checksum = response[-6:-2]
-        if _calculated_checksum != _message_checksum:
-            raise InvalidMessageChecksum(
-                f"Invalid checksum for {self.__class__.__name__}, expected {str(_calculated_checksum)} got {str(_message_checksum)}",
-            )
-        if len(response) != len(self):
-            raise InvalidMessageLength(
-                f"Invalid message length received for {self.__class__.__name__}, expected {str(len(self))} bytes got {str(len(response))}"
-            )
-
-        self.msg_id = response[4:8]
-        self.seq_id = response[8:12]
-        response = response[12:]
-        if self.format_size in [MESSAGE_SMALL, MESSAGE_LARGE]:
-            self.ack_id = response[:4]
-            response = response[4:]
-        if self.format_size != MESSAGE_SMALL:
-            self.mac = response[:16]
-            response = response[16:]
-        response = self._parse_params(response)
-
-        _args = b"".join(a.serialize() for a in self.args)
-        msg = self.ID
-        if self.mac != "":
-            msg += self.mac
-        msg += _args
-
-    def _parse_params(self, response):
-        for param in self.params:
-            my_val = response[: len(param)]
-            param.deserialize(my_val)
-            response = response[len(my_val) :]
-        return response
-
-    def __len__(self):
-        arglen = sum(len(x) for x in self.params)
-        return 34 + arglen + self.len_correction
-
-
-class NodeAckSmallResponse(NodeResponse):
-    """
-    Acknowledge message without source MAC
-
-    Response to: Any message
-    """
-
-    ID = b"0000"
-
-    def __init__(self):
-        super().__init__(MESSAGE_SMALL)
-
-
-class NodeAckLargeResponse(NodeResponse):
-    """
-    Acknowledge message with source MAC
-
-    Response to: Any message
-    """
-
-    ID = b"0000"
-
-    def __init__(self):
-        super().__init__(MESSAGE_LARGE)
-
-
-class CirclePlusQueryResponse(NodeResponse):
-    """
-    TODO:
-
-    Response to : ???
-    """
-
-    ID = b"0002"
-
-    def __init__(self):
-        super().__init__()
-        self.channel = String(None, length=2)
-        self.source_mac_id = String(None, length=16)
-        self.extended_pan_id = String(None, length=16)
-        self.unique_network_id = String(None, length=16)
-        self.new_node_mac_id = String(None, length=16)
-        self.pan_id = String(None, length=4)
-        self.idx = Int(0, length=2)
-        self.params += [
-            self.channel,
-            self.source_mac_id,
-            self.extended_pan_id,
-            self.unique_network_id,
-            self.new_node_mac_id,
-            self.pan_id,
-            self.idx,
-        ]
-
-    def __len__(self):
-        arglen = sum(len(x) for x in self.params)
-        return 18 + arglen
-
-    def deserialize(self, response):
-        super().deserialize(response)
-        # Clear first two characters of mac ID, as they contain part of the short PAN-ID
-        self.new_node_mac_id.value = b"00" + self.new_node_mac_id.value[2:]
-
-
-class CirclePlusQueryEndResponse(NodeResponse):
-    """
-    TODO:
-        PWAckReplyV1_0
-        <argument name="code" length="2"/>
-
-    Response to : ???
-    """
-
-    ID = b"0003"
-
-    def __init__(self):
-        super().__init__()
-        self.status = Int(0, 4)
-        self.params += [self.status]
-
-    def __len__(self):
-        arglen = sum(len(x) for x in self.params)
-        return 18 + arglen
-
-
-class CirclePlusConnectResponse(NodeResponse):
-    """
-    CirclePlus connected to the network
-
-    Response to : CirclePlusConnectRequest
-    """
-
-    ID = b"0005"
-
-    def __init__(self):
-        super().__init__()
-        self.existing = Int(0, 2)
-        self.allowed = Int(0, 2)
-        self.params += [self.existing, self.allowed]
-
-    def __len__(self):
-        arglen = sum(len(x) for x in self.params)
-        return 18 + arglen
-
-
-class NodeJoinAvailableResponse(NodeResponse):
-    """
-    Message from an unjoined node to notify it is available to join a plugwise network
-
-    Response to : <nothing>
-    """
-
-    ID = b"0006"
-
-
-class StickInitResponse(NodeResponse):
-    """
-    Returns the configuration and status of the USB-Stick
-
-    Optional:
-    - circle_plus_mac
-    - network_id
-
-    <argument name="upYesNo" length="2"/>
-    <argument name="extendedPanId" length="16" optional="1"/>
-    <argument name="panId" length="4" optional="1"/>
-
-    Response to: StickInitRequest
-    """
-
-    ID = b"0011"
-
-    def __init__(self):
-        super().__init__()
-        self.unknown1 = Int(0, length=2)
-        self.network_is_online = Int(0, length=2)
-        self.circle_plus_mac = String(None, length=16)
-        self.network_id = Int(0, 4, False)
-        self.unknown2 = Int(0, length=2)
-        self.params += [
-            self.unknown1,
-            self.network_is_online,
-            self.circle_plus_mac,
-            self.network_id,
-            self.unknown2,
-        ]
-
-
-class NodePingResponse(NodeResponse):
-    """
-    Ping response from node
-
-    - incomingLastHopRssiTarget (received signal strength indicator)
-    - lastHopRssiSource
-    - timediffInMs
-
-    Response to : NodePingRequest
-    """
-
-    ID = b"000E"
-
-    def __init__(self):
-        super().__init__()
-        self.rssi_in = Int(0, length=2)
-        self.rssi_out = Int(0, length=2)
-        self.ping_ms = Int(0, 4, False)
-        self.params += [
-            self.rssi_in,
-            self.rssi_out,
-            self.ping_ms,
-        ]
-
-
-class CirclePowerUsageResponse(NodeResponse):
-    """
-    Returns power usage as impulse counters for several different timeframes
-
-    Response to : CirclePowerUsageRequest
-    """
-
-    ID = b"0013"
-
-    def __init__(self):
-        super().__init__()
-        self.pulse_1s = Int(0, 4)
-        self.pulse_8s = Int(0, 4)
-        self.pulse_hour_consumed = Int(0, 8)
-        self.pulse_hour_produced = Int(0, 8)
-        self.nanosecond_offset = Int(0, 4)
-        self.params += [
-            self.pulse_1s,
-            self.pulse_8s,
-            self.pulse_hour_consumed,
-            self.pulse_hour_produced,
-            self.nanosecond_offset,
-        ]
-
-
-class CirclePlusScanResponse(NodeResponse):
-    """
-    Returns the MAC of a registered node at the specified memory address
-
-    Response to: CirclePlusScanRequest
-    """
-
-    ID = b"0019"
-
-    def __init__(self):
-        super().__init__()
-        self.node_mac = String(None, length=16)
-        self.node_address = Int(0, 2, False)
-        self.params += [self.node_mac, self.node_address]
-
-
-class NodeRemoveResponse(NodeResponse):
-    """
-    Returns conformation (or not) if node is removed from the Plugwise network
-    by having it removed from the memory of the Circle+
-
-    Response to: NodeRemoveRequest
-    """
-
-    ID = b"001D"
-
-    def __init__(self):
-        super().__init__()
-        self.node_mac_id = String(None, length=16)
-        self.status = Int(0, 2)
-        self.params += [self.node_mac_id, self.status]
-
-
-class NodeInfoResponse(NodeResponse):
-    """
-    Returns the status information of Node
-
-    Response to: NodeInfoRequest
-    """
-
-    ID = b"0024"
-
-    def __init__(self):
-        super().__init__()
-        self.datetime = DateTime()
-        self.last_logaddr = LogAddr(0, length=8)
-        self.relay_state = Int(0, length=2)
-        #  TODO: 20220126 snake-style
-        #  pylint: disable=invalid-name
-        self.hz = Int(0, length=2)
-        self.hw_ver = String(None, length=12)
-        self.fw_ver = UnixTimestamp(0)
-        self.node_type = Int(0, length=2)
-        self.params += [
-            self.datetime,
-            self.last_logaddr,
-            self.relay_state,
-            self.hz,
-            self.hw_ver,
-            self.fw_ver,
-            self.node_type,
-        ]
-
-
-class CircleCalibrationResponse(NodeResponse):
-    """
-    returns the calibration settings of node
-
-    Response to: CircleCalibrationRequest
-    """
-
-    ID = b"0027"
-
-    def __init__(self):
-        super().__init__()
-        self.gain_a = Float(0, 8)
-        self.gain_b = Float(0, 8)
-        self.off_tot = Float(0, 8)
-        self.off_noise = Float(0, 8)
-        self.params += [self.gain_a, self.gain_b, self.off_tot, self.off_noise]
-
-
-class CirclePlusRealTimeClockResponse(NodeResponse):
-    """
-    returns the real time clock of CirclePlus node
-
-    Response to: CirclePlusRealTimeClockGetRequest
-    """
-
-    ID = b"003A"
-
-    def __init__(self):
-        super().__init__()
-
-        self.time = RealClockTime()
-        self.day_of_week = Int(0, 2, False)
-        self.date = RealClockDate()
-        self.params += [self.time, self.day_of_week, self.date]
-
-
-class CircleClockResponse(NodeResponse):
-    """
-    Returns the current internal clock of Node
-
-    Response to: CircleClockGetRequest
-    """
-
-    ID = b"003F"
-
-    def __init__(self):
-        super().__init__()
-        self.time = Time()
-        self.day_of_week = Int(0, 2, False)
-        self.unknown = Int(0, 2)
-        self.unknown2 = Int(0, 4)
-        self.params += [self.time, self.day_of_week, self.unknown, self.unknown2]
-
-
-class CircleEnergyCountersResponse(NodeResponse):
-    """
-    Returns historical energy usage of requested memory address
-    Each response contains 4 energy counters at specified 1 hour timestamp
-
-    Response to: CircleEnergyCountersRequest
-    """
-
-    ID = b"0049"
-
-    def __init__(self):
-        super().__init__()
-        self.logdate1 = DateTime()
-        self.pulses1 = Int(0, 8)
-        self.logdate2 = DateTime()
-        self.pulses2 = Int(0, 8)
-        self.logdate3 = DateTime()
-        self.pulses3 = Int(0, 8)
-        self.logdate4 = DateTime()
-        self.pulses4 = Int(0, 8)
-        self.logaddr = LogAddr(0, length=8)
-        self.params += [
-            self.logdate1,
-            self.pulses1,
-            self.logdate2,
-            self.pulses2,
-            self.logdate3,
-            self.pulses3,
-            self.logdate4,
-            self.pulses4,
-            self.logaddr,
-        ]
-
-
-class NodeAwakeResponse(NodeResponse):
-    """
-    A sleeping end device (SED: Scan, Sense, Switch) sends
-    this message to announce that is awake. Awake types:
-    - 0 : The SED joins the network for maintenance
-    - 1 : The SED joins a network for the first time
-    - 2 : The SED joins a network it has already joined, e.g. after reinserting a battery
-    - 3 : When a SED switches a device group or when reporting values such as temperature/humidity
-    - 4 : TODO: Unknown
-    - 5 : A human pressed the button on a SED to wake it up
-
-    Response to: <nothing>
-    """
-
-    ID = b"004F"
-
-    def __init__(self):
-        super().__init__()
-        self.awake_type = Int(0, 2, False)
-        self.params += [self.awake_type]
-
-
-class NodeSwitchGroupResponse(NodeResponse):
-    """
-    A sleeping end device (SED: Scan, Sense, Switch) sends
-    this message to switch groups on/off when the configured
-    switching conditions have been met.
-
-    Response to: <nothing>
-    """
-
-    ID = b"0056"
-
-    def __init__(self):
-        super().__init__()
-        self.group = Int(0, 2, False)
-        self.power_state = Int(0, length=2)
-        self.params += [
-            self.group,
-            self.power_state,
-        ]
-
-
-class NodeFeaturesResponse(NodeResponse):
-    """
-    Returns supported features of node
-    TODO: FeatureBitmask
-
-    Response to: NodeFeaturesRequest
-    """
-
-    ID = b"0060"
-
-    def __init__(self):
-        super().__init__()
-        self.features = String(None, length=16)
-        self.params += [self.features]
-
-
-class NodeJoinAckResponse(NodeResponse):
-    """
-    Notification message when node (re)joined existing network again.
-    Sent when a SED (re)joins the network e.g. when you reinsert the battery of a Scan
-
-    Response to: <nothing> or NodeAddRequest
-    """
-
-    ID = b"0061"
-
-    def __init__(self):
-        super().__init__()
-        # sequence number is always FFFD
-
-
-class NodeAckResponse(NodeResponse):
-    """
-    Acknowledge message in regular format
-    Sent by nodes supporting plugwise 2.4 protocol version
-
-    Response to:
-    """
-
-    ID = b"0100"
-
-    def __init__(self):
-        super().__init__()
-        self.ack_id = Int(0, 2, False)
-
-
-class SenseReportResponse(NodeResponse):
-    """
-    Returns the current temperature and humidity of a Sense node.
-    The interval this report is sent is configured by the 'SenseReportIntervalRequest' request
-
-    Response to: <nothing>
-    """
-
-    ID = b"0105"
-
-    def __init__(self):
-        super().__init__()
-        self.humidity = Int(0, length=4)
-        self.temperature = Int(0, length=4)
-        self.params += [self.humidity, self.temperature]
-
-
-class CircleInitialRelaisStateResponse(NodeResponse):
-    """
-    Returns the initial relais state.
-
-    Response to: CircleInitialRelaisStateRequest
-    """
-
-    ID = b"0139"
-
-    def __init__(self):
-        super().__init__()
-        set_or_get = Int(0, length=2)
-        relais = Int(0, length=2)
-        self.params += [set_or_get, relais]
-
-
-id_to_message = {
-    b"0002": CirclePlusQueryResponse(),
-    b"0003": CirclePlusQueryEndResponse(),
-    b"0005": CirclePlusConnectResponse(),
-    b"0006": NodeJoinAvailableResponse(),
-    b"000E": NodePingResponse(),
-    b"0011": StickInitResponse(),
-    b"0013": CirclePowerUsageResponse(),
-    b"0019": CirclePlusScanResponse(),
-    b"001D": NodeRemoveResponse(),
-    b"0024": NodeInfoResponse(),
-    b"0027": CircleCalibrationResponse(),
-    b"003A": CirclePlusRealTimeClockResponse(),
-    b"003F": CircleClockResponse(),
-    b"0049": CircleEnergyCountersResponse(),
-    b"0060": NodeFeaturesResponse(),
-    b"0100": NodeAckResponse(),
-    b"0105": SenseReportResponse(),
-}
-
-
-def get_message_response(message_id, length, seq_id):
-    """
-    Return message class based on sequence ID, Length of message or message ID.
-    """
-    # First check for known sequence ID's
-    if seq_id == b"FFFD":
-        return NodeJoinAckResponse()
-    if seq_id == b"FFFE":
-        return NodeAwakeResponse()
-    if seq_id == b"FFFF":
-        return NodeSwitchGroupResponse()
-
-    # No fixed sequence ID, continue at message ID
-    if message_id == b"0000":
-        if length == 20:
-            return NodeAckSmallResponse()
-        if length == 36:
-            return NodeAckLargeResponse()
-        return None
-    return id_to_message.get(message_id, None)
diff --git a/plugwise/stick/nodes/__init__.py b/plugwise/stick/nodes/__init__.py
deleted file mode 100644
index f5fec0e8e..000000000
--- a/plugwise/stick/nodes/__init__.py
+++ /dev/null
@@ -1,297 +0,0 @@
-"""Plugwise nodes."""
-from datetime import datetime
-import logging
-
-from plugwise.stick.util import validate_mac, version_to_model
-
-from ..constants import (
-    FEATURE_AVAILABLE,
-    FEATURE_PING,
-    FEATURE_RELAY,
-    FEATURE_RSSI_IN,
-    FEATURE_RSSI_OUT,
-    HW_MODELS,
-    PRIORITY_LOW,
-    UTF8_DECODE,
-)
-from ..messages.requests import NodeFeaturesRequest, NodeInfoRequest, NodePingRequest
-from ..messages.responses import (
-    NodeFeaturesResponse,
-    NodeInfoResponse,
-    NodeJoinAckResponse,
-    NodePingResponse,
-)
-
-_LOGGER = logging.getLogger(__name__)
-
-
-class PlugwiseNode:
-    """Base class for a Plugwise node."""
-
-    def __init__(self, mac, address, message_sender):
-        mac = mac.upper()
-        if not validate_mac(mac):
-            _LOGGER.warning(
-                "MAC address is in unexpected format: %s",
-                str(mac),
-            )
-        self._mac = bytes(mac, encoding=UTF8_DECODE)
-        self.message_sender = message_sender
-        self._features = ()
-        self._address = address
-        self._callbacks = {}
-        self._last_update = None
-        self._available = False
-        self._battery_powered = False
-        self._measures_power = False
-        self._rssi_in = None
-        self._rssi_out = None
-        self._ping = None
-        self._node_type = None
-        self._hardware_version = None
-        self._firmware_version = None
-        self._relay_state = False
-        self._last_log_address = None
-        self._device_features = None
-
-    @property
-    def available(self) -> bool:
-        """Current network state of plugwise node."""
-        return self._available
-
-    @available.setter
-    def available(self, state: bool):
-        """Set current network availability state of plugwise node."""
-        if state:
-            if not self._available:
-                self._available = True
-                _LOGGER.debug(
-                    "Mark node %s available",
-                    self.mac,
-                )
-                self.do_callback(FEATURE_AVAILABLE["id"])
-        else:
-            if self._available:
-                self._available = False
-                _LOGGER.debug(
-                    "Mark node %s unavailable",
-                    self.mac,
-                )
-                self.do_callback(FEATURE_AVAILABLE["id"])
-
-    @property
-    def battery_powered(self) -> bool:
-        """Return True if node is a SED (battery powered) device."""
-        return self._battery_powered
-
-    @property
-    def hardware_model(self) -> str:
-        """Return hardware model."""
-        if self._hardware_version:
-            return version_to_model(self._hardware_version)
-        return None
-
-    @property
-    def hardware_version(self) -> str:
-        """Return hardware version."""
-        if self._hardware_version is not None:
-            return self._hardware_version
-        return "Unknown"
-
-    @property
-    def features(self) -> tuple:
-        """Return the abstracted features supported by this plugwise device."""
-        return self._features
-
-    @property
-    def firmware_version(self) -> str:
-        """Return firmware version."""
-        if self._firmware_version is not None:
-            return str(self._firmware_version)
-        return "Unknown"
-
-    @property
-    def last_update(self) -> datetime:
-        """Return datetime of last received update."""
-        return self._last_update
-
-    @property
-    def mac(self) -> str:
-        """Return the MAC address in string."""
-        return self._mac.decode(UTF8_DECODE)
-
-    @property
-    def measures_power(self) -> bool:
-        """Return True if node can measure power usage."""
-        return self._measures_power
-
-    @property
-    def name(self) -> str:
-        """Return unique name."""
-        return self.hardware_model + " (" + str(self._address) + ")"
-
-    @property
-    def ping(self) -> int:
-        """Return ping roundtrip in ms."""
-        if self._ping is not None:
-            return self._ping
-        return 0
-
-    @property
-    def rssi_in(self) -> int:
-        """Return inbound RSSI level."""
-        if self._rssi_in is not None:
-            return self._rssi_in
-        return 0
-
-    @property
-    def rssi_out(self) -> int:
-        """Return outbound RSSI level, based on inbound RSSI level of neighbor node."""
-        if self._rssi_out is not None:
-            return self._rssi_out
-        return 0
-
-    def do_ping(self, callback=None):
-        """Send network ping message to node."""
-        self._request_ping(callback, True)
-
-    def _request_info(self, callback=None):
-        """Request info from node."""
-        self.message_sender(
-            NodeInfoRequest(self._mac),
-            callback,
-            0,
-            PRIORITY_LOW,
-        )
-
-    def _request_features(self, callback=None):
-        """Request supported features for this node."""
-        self.message_sender(
-            NodeFeaturesRequest(self._mac),
-            callback,
-        )
-
-    def _request_ping(self, callback=None, ignore_sensor=True):
-        """Ping node."""
-        if ignore_sensor or FEATURE_PING["id"] in self._callbacks:
-            self.message_sender(
-                NodePingRequest(self._mac),
-                callback,
-            )
-
-    def message_for_node(self, message):
-        """Process received message."""
-        if message.mac == self._mac:
-            if message.timestamp is not None:
-                _LOGGER.debug(
-                    "Previous update %s of node %s, last message %s",
-                    str(self._last_update),
-                    self.mac,
-                    str(message.timestamp),
-                )
-                self._last_update = message.timestamp
-            if not self._available:
-                self.available = True
-                self._request_info()
-            if isinstance(message, NodePingResponse):
-                self._process_ping_response(message)
-            elif isinstance(message, NodeInfoResponse):
-                self._process_info_response(message)
-            elif isinstance(message, NodeFeaturesResponse):
-                self._process_features_response(message)
-            elif isinstance(message, NodeJoinAckResponse):
-                self._process_join_ack_response(message)
-            else:
-                self.message_for_circle(message)
-                self.message_for_sed(message)
-        else:
-            _LOGGER.debug(
-                "Skip message, mac of node (%s) != mac at message (%s)",
-                message.mac.decode(UTF8_DECODE),
-                self.mac,
-            )
-
-    def message_for_circle(self, message):
-        """Pass messages to PlugwiseCircle class"""
-
-    def message_for_sed(self, message):
-        """Pass messages to NodeSED class"""
-
-    def subscribe_callback(self, callback, sensor) -> bool:
-        """Subscribe callback to execute when state change happens."""
-        if sensor in self._features:
-            if sensor not in self._callbacks:
-                self._callbacks[sensor] = []
-            self._callbacks[sensor].append(callback)
-            return True
-        return False
-
-    def unsubscribe_callback(self, callback, sensor):
-        """Unsubscribe callback to execute when state change happens."""
-        if sensor in self._callbacks:
-            self._callbacks[sensor].remove(callback)
-
-    def do_callback(self, sensor):
-        """Execute callbacks registered for specified callback type."""
-        if sensor in self._callbacks:
-            for callback in self._callbacks[sensor]:
-                try:
-                    callback(None)
-                # TODO: narrow exception
-                except Exception as err:  # pylint: disable=broad-except
-                    _LOGGER.error(
-                        "Error while executing all callback : %s",
-                        err,
-                    )
-
-    def _process_join_ack_response(self, message):
-        """Process join acknowledge response message"""
-        _LOGGER.info(
-            "Node %s has (re)joined plugwise network",
-            self.mac,
-        )
-
-    def _process_ping_response(self, message):
-        """Process ping response message."""
-        if self._rssi_in != message.rssi_in.value:
-            self._rssi_in = message.rssi_in.value
-            self.do_callback(FEATURE_RSSI_IN["id"])
-        if self._rssi_out != message.rssi_out.value:
-            self._rssi_out = message.rssi_out.value
-            self.do_callback(FEATURE_RSSI_OUT["id"])
-        if self._ping != message.ping_ms.value:
-            self._ping = message.ping_ms.value
-            self.do_callback(FEATURE_PING["id"])
-
-    def _process_info_response(self, message):
-        """Process info response message."""
-        _LOGGER.debug(
-            "Response info message for node %s, last log address %s",
-            self.mac,
-            str(message.last_logaddr.value),
-        )
-        if message.relay_state.serialize() == b"01":
-            if not self._relay_state:
-                self._relay_state = True
-                self.do_callback(FEATURE_RELAY["id"])
-        else:
-            if self._relay_state:
-                self._relay_state = False
-                self.do_callback(FEATURE_RELAY["id"])
-        self._hardware_version = message.hw_ver.value.decode(UTF8_DECODE)
-        self._firmware_version = message.fw_ver.value
-        self._node_type = message.node_type.value
-        if self._last_log_address != message.last_logaddr.value:
-            self._last_log_address = message.last_logaddr.value
-        _LOGGER.debug("Node type        = %s", self.hardware_model)
-        if not self._battery_powered:
-            _LOGGER.debug("Relay state      = %s", str(self._relay_state))
-        _LOGGER.debug("Hardware version = %s", str(self._hardware_version))
-        _LOGGER.debug("Firmware version = %s", str(self._firmware_version))
-
-    def _process_features_response(self, message):
-        """Process features message."""
-        _LOGGER.warning(
-            "Node %s supports features %s", self.mac, str(message.features.value)
-        )
-        self._device_features = message.features.value
diff --git a/plugwise/stick/nodes/circle.py b/plugwise/stick/nodes/circle.py
deleted file mode 100644
index fcdd2006c..000000000
--- a/plugwise/stick/nodes/circle.py
+++ /dev/null
@@ -1,877 +0,0 @@
-"""Plugwise Circle node object."""
-from datetime import datetime, timedelta
-import logging
-
-from ..constants import (
-    FEATURE_ENERGY_CONSUMPTION_TODAY,
-    FEATURE_PING,
-    FEATURE_POWER_CONSUMPTION_CURRENT_HOUR,
-    FEATURE_POWER_CONSUMPTION_PREVIOUS_HOUR,
-    FEATURE_POWER_CONSUMPTION_TODAY,
-    FEATURE_POWER_CONSUMPTION_YESTERDAY,
-    FEATURE_POWER_PRODUCTION_CURRENT_HOUR,
-    FEATURE_POWER_USE,
-    FEATURE_POWER_USE_LAST_8_SEC,
-    FEATURE_RELAY,
-    FEATURE_RSSI_IN,
-    FEATURE_RSSI_OUT,
-    MAX_TIME_DRIFT,
-    MESSAGE_TIME_OUT,
-    PRIORITY_HIGH,
-    PRIORITY_LOW,
-    PULSES_PER_KW_SECOND,
-    RELAY_SWITCHED_OFF,
-    RELAY_SWITCHED_ON,
-)
-from ..messages.requests import (
-    CircleCalibrationRequest,
-    CircleClockGetRequest,
-    CircleClockSetRequest,
-    CircleEnergyCountersRequest,
-    CirclePowerUsageRequest,
-    CircleSwitchRelayRequest,
-)
-from ..messages.responses import (
-    CircleCalibrationResponse,
-    CircleClockResponse,
-    CircleEnergyCountersResponse,
-    CirclePowerUsageResponse,
-    NodeAckLargeResponse,
-)
-from ..nodes import PlugwiseNode
-
-_LOGGER = logging.getLogger(__name__)
-
-
-class PlugwiseCircle(PlugwiseNode):
-    """provides interface to the Plugwise Circle nodes and base class for Circle+ nodes"""
-
-    def __init__(self, mac, address, message_sender):
-        super().__init__(mac, address, message_sender)
-        self._features = (
-            FEATURE_ENERGY_CONSUMPTION_TODAY["id"],
-            FEATURE_PING["id"],
-            FEATURE_POWER_USE["id"],
-            FEATURE_POWER_USE_LAST_8_SEC["id"],
-            FEATURE_POWER_CONSUMPTION_CURRENT_HOUR["id"],
-            FEATURE_POWER_CONSUMPTION_PREVIOUS_HOUR["id"],
-            FEATURE_POWER_CONSUMPTION_TODAY["id"],
-            FEATURE_POWER_CONSUMPTION_YESTERDAY["id"],
-            FEATURE_POWER_PRODUCTION_CURRENT_HOUR["id"],
-            # FEATURE_POWER_PRODUCTION_PREVIOUS_HOUR["id"],
-            FEATURE_RSSI_IN["id"],
-            FEATURE_RSSI_OUT["id"],
-            FEATURE_RELAY["id"],
-        )
-        self._energy_consumption_today_reset = datetime.now().replace(
-            hour=0, minute=0, second=0, microsecond=0
-        )
-        self._energy_memory = {}
-        self._energy_history_collecting = False
-        self._energy_history_collecting_timestamp = datetime.now()
-        self._energy_history = {}
-        self._energy_last_collected_timestamp = datetime(2000, 1, 1)
-        self._energy_last_rollover_timestamp = datetime(2000, 1, 1)
-        self._energy_last_local_hour = datetime.now().hour
-        self._energy_last_populated_slot = 0
-        self._energy_pulses_current_hour = None
-        self._energy_pulses_prev_hour = None
-        self._energy_rollover_day_started = False
-        self._energy_rollover_day_finished = True
-        self._energy_rollover_history_started = False
-        self._energy_rollover_history_finished = True
-        self._energy_rollover_hour_started = False
-        self._energy_rollover_hour_finished = True
-        self._energy_pulses_today_hourly = None
-        self._energy_pulses_today_now = None
-        self._energy_pulses_yesterday = None
-        self._new_relay_state = False
-        self._new_relay_stamp = datetime.now() - timedelta(seconds=MESSAGE_TIME_OUT)
-        self._pulses_1s = None
-        self._pulses_8s = None
-        self._pulses_produced_1h = None
-        self.calibration = False
-        self._gain_a = None
-        self._gain_b = None
-        self._off_noise = None
-        self._off_tot = None
-        self._measures_power = True
-        self._last_log_collected = False
-        self.timezone_delta = datetime.now().replace(
-            minute=0, second=0, microsecond=0
-        ) - datetime.utcnow().replace(minute=0, second=0, microsecond=0)
-        self._clock_offset = None
-        self.get_clock(self.sync_clock)
-        self._request_calibration()
-
-    @property
-    def current_power_usage(self):
-        """
-        Returns power usage during the last second in Watts
-        Based on last received power usage information
-        """
-        if self._pulses_1s is not None:
-            return self.pulses_to_kws(self._pulses_1s) * 1000
-        return None
-
-    @property
-    def current_power_usage_8_sec(self):
-        """
-        Returns power usage during the last 8 second in Watts
-        Based on last received power usage information
-        """
-        if self._pulses_8s is not None:
-            return self.pulses_to_kws(self._pulses_8s, 8) * 1000
-        return None
-
-    @property
-    def energy_consumption_today(self) -> float:
-        """Returns total energy consumption since midnight in kWh"""
-        if self._energy_pulses_today_now is not None:
-            return self.pulses_to_kws(self._energy_pulses_today_now, 3600)
-        return None
-
-    @property
-    def energy_consumption_today_last_reset(self):
-        """Last reset of total energy consumption today"""
-        return self._energy_consumption_today_reset
-
-    @property
-    def power_consumption_current_hour(self):
-        """
-        Returns the power usage during this running hour in kWh
-        Based on last received power usage information
-        """
-        if self._energy_pulses_current_hour is not None:
-            return self.pulses_to_kws(self._energy_pulses_current_hour, 3600)
-        return None
-
-    @property
-    def power_consumption_previous_hour(self):
-        """Returns power consumption during the previous hour in kWh"""
-        if self._energy_pulses_prev_hour is not None:
-            return self.pulses_to_kws(self._energy_pulses_prev_hour, 3600)
-        return None
-
-    @property
-    def power_consumption_today(self):
-        """Total power consumption during today in kWh"""
-        if self._energy_pulses_today_hourly is not None:
-            return self.pulses_to_kws(self._energy_pulses_today_hourly, 3600)
-        return None
-
-    @property
-    def power_consumption_yesterday(self):
-        """Total power consumption of yesterday in kWh"""
-        if self._energy_pulses_yesterday is not None:
-            return self.pulses_to_kws(self._energy_pulses_yesterday, 3600)
-        return None
-
-    @property
-    def power_production_current_hour(self):
-        """
-        Returns the power production during this running hour in kWh
-        Based on last received power usage information
-        """
-        if self._pulses_produced_1h is not None:
-            return self.pulses_to_kws(self._pulses_produced_1h, 3600)
-        return None
-
-    @property
-    def relay_state(self) -> bool:
-        """
-        Return last known relay state or the new switch state by anticipating
-        the acknowledge for new state is getting in before message timeout.
-        """
-        if self._new_relay_stamp + timedelta(seconds=MESSAGE_TIME_OUT) > datetime.now():
-            return self._new_relay_state
-        return self._relay_state
-
-    @relay_state.setter
-    def relay_state(self, state):
-        """Request the relay to switch state."""
-        self._request_switch(state)
-        self._new_relay_state = state
-        self._new_relay_stamp = datetime.now()
-        if state != self._relay_state:
-            self.do_callback(FEATURE_RELAY["id"])
-
-    def _request_calibration(self, callback=None):
-        """Request calibration info"""
-        self.message_sender(
-            CircleCalibrationRequest(self._mac),
-            callback,
-            0,
-            PRIORITY_HIGH,
-        )
-
-    def _request_switch(self, state, callback=None):
-        """Request to switch relay state and request state info"""
-        self.message_sender(
-            CircleSwitchRelayRequest(self._mac, state),
-            callback,
-            0,
-            PRIORITY_HIGH,
-        )
-
-    def request_power_update(self, callback=None):
-        """Request power usage and update energy counters"""
-        if self._available:
-            self.message_sender(
-                CirclePowerUsageRequest(self._mac),
-                callback,
-            )
-            if len(self._energy_history) > 0:
-                # Request new energy counters if last one is more than one hour ago
-                if self._energy_last_collected_timestamp < datetime.utcnow().replace(
-                    minute=0, second=0, microsecond=0
-                ):
-                    self.request_energy_counters()
-            else:
-                # No history collected yet, request energy history
-                self.request_energy_counters()
-
-    def message_for_circle(self, message):
-        """
-        Process received message
-        """
-        if isinstance(message, CirclePowerUsageResponse):
-            if self.calibration:
-                self._response_power_usage(message)
-                _LOGGER.debug(
-                    "Power update for %s, last update %s",
-                    self.mac,
-                    str(self._last_update),
-                )
-            else:
-                _LOGGER.info(
-                    "Received power update for %s before calibration information is known",
-                    self.mac,
-                )
-                self._request_calibration(self.request_power_update)
-        elif isinstance(message, NodeAckLargeResponse):
-            self._node_ack_response(message)
-        elif isinstance(message, CircleCalibrationResponse):
-            self._response_calibration(message)
-        elif isinstance(message, CircleEnergyCountersResponse):
-            if self.calibration:
-                self._response_energy_counters(message)
-            else:
-                _LOGGER.debug(
-                    "Received power buffer log for %s before calibration information is known",
-                    self.mac,
-                )
-                self._request_calibration(self.request_energy_counters)
-        elif isinstance(message, CircleClockResponse):
-            self._response_clock(message)
-        else:
-            self.message_for_circle_plus(message)
-
-    def message_for_circle_plus(self, message):
-        """Pass messages to PlugwiseCirclePlus class"""
-
-    def _node_ack_response(self, message):
-        """Process switch response message"""
-        if message.ack_id == RELAY_SWITCHED_ON:
-            if not self._relay_state:
-                _LOGGER.debug(
-                    "Switch relay on for %s",
-                    self.mac,
-                )
-                self._relay_state = True
-                self.do_callback(FEATURE_RELAY["id"])
-        elif message.ack_id == RELAY_SWITCHED_OFF:
-            if self._relay_state:
-                _LOGGER.debug(
-                    "Switch relay off for %s",
-                    self.mac,
-                )
-                self._relay_state = False
-                self.do_callback(FEATURE_RELAY["id"])
-        else:
-            _LOGGER.debug(
-                "Unmanaged _node_ack_response %s received for %s",
-                str(message.ack_id),
-                self.mac,
-            )
-
-    def _response_power_usage(self, message: CirclePowerUsageResponse):
-        # Sometimes the circle returns -1 for some of the pulse counters
-        # likely this means the circle measures very little power and is suffering from
-        # rounding errors. Zero these out. However, negative pulse values are valid
-        # for power producing appliances, like solar panels, so don't complain too loudly.
-
-        # Power consumption last second
-        if message.pulse_1s.value == -1:
-            message.pulse_1s.value = 0
-            _LOGGER.debug(
-                "1 sec power pulse counter for node %s has value of -1, corrected to 0",
-                self.mac,
-            )
-        self._pulses_1s = message.pulse_1s.value
-        if message.pulse_1s.value != 0:
-            if message.nanosecond_offset.value != 0:
-                pulses_1s = (
-                    message.pulse_1s.value
-                    * (1000000000 + message.nanosecond_offset.value)
-                ) / 1000000000
-            else:
-                pulses_1s = message.pulse_1s.value
-            self._pulses_1s = pulses_1s
-        else:
-            self._pulses_1s = 0
-        self.do_callback(FEATURE_POWER_USE["id"])
-        # Power consumption last 8 seconds
-        if message.pulse_8s.value == -1:
-            message.pulse_8s.value = 0
-            _LOGGER.debug(
-                "8 sec power pulse counter for node %s has value of -1, corrected to 0",
-                self.mac,
-            )
-        if message.pulse_8s.value != 0:
-            if message.nanosecond_offset.value != 0:
-                pulses_8s = (
-                    message.pulse_8s.value
-                    * (1000000000 + message.nanosecond_offset.value)
-                ) / 1000000000
-            else:
-                pulses_8s = message.pulse_8s.value
-            self._pulses_8s = pulses_8s
-        else:
-            self._pulses_8s = 0
-        self.do_callback(FEATURE_POWER_USE_LAST_8_SEC["id"])
-        # Power consumption current hour
-        if message.pulse_hour_consumed.value == -1:
-            _LOGGER.debug(
-                "1 hour consumption power pulse counter for node %s has value of -1, drop value",
-                self.mac,
-            )
-        else:
-            self._update_energy_current_hour(message.pulse_hour_consumed.value)
-
-        # Power produced current hour
-        if message.pulse_hour_produced.value == -1:
-            message.pulse_hour_produced.value = 0
-            _LOGGER.debug(
-                "1 hour power production pulse counter for node %s has value of -1, corrected to 0",
-                self.mac,
-            )
-        if self._pulses_produced_1h != message.pulse_hour_produced.value:
-            self._pulses_produced_1h = message.pulse_hour_produced.value
-            self.do_callback(FEATURE_POWER_PRODUCTION_CURRENT_HOUR["id"])
-
-    def _response_calibration(self, message: CircleCalibrationResponse):
-        """Store calibration properties"""
-        for calibration in ("gain_a", "gain_b", "off_noise", "off_tot"):
-            val = getattr(message, calibration).value
-            setattr(self, "_" + calibration, val)
-        self.calibration = True
-
-    def pulses_to_kws(self, pulses, seconds=1):
-        """
-        converts the amount of pulses to kWs using the calaboration offsets
-        """
-        if pulses is None:
-            return None
-        if pulses == 0 or not self.calibration:
-            return 0.0
-        pulses_per_s = pulses / float(seconds)
-        corrected_pulses = seconds * (
-            (
-                (((pulses_per_s + self._off_noise) ** 2) * self._gain_b)
-                + ((pulses_per_s + self._off_noise) * self._gain_a)
-            )
-            + self._off_tot
-        )
-        calc_value = corrected_pulses / PULSES_PER_KW_SECOND / seconds
-        # Fix minor miscalculations
-        if -0.001 < calc_value < 0.001:
-            calc_value = 0.0
-        return calc_value
-
-    def _collect_energy_pulses(self, start_utc: datetime, end_utc: datetime):
-        """Return energy pulses of given hours"""
-
-        if start_utc == end_utc:
-            hours = 0
-        else:
-            hours = int((end_utc - start_utc).seconds / 3600)
-        _energy_history_failed = False
-        _energy_pulses = 0
-        for hour in range(0, hours + 1):
-            _log_timestamp = start_utc + timedelta(hours=hour)
-            if self._energy_history.get(_log_timestamp) is not None:
-                _energy_pulses += self._energy_history[_log_timestamp]
-                _LOGGER.debug(
-                    "_collect_energy_pulses for %s | %s : %s, total = %s",
-                    self.mac,
-                    str(_log_timestamp),
-                    str(self._energy_history[_log_timestamp]),
-                    str(_energy_pulses),
-                )
-            else:
-                _mem_address = self._energy_timestamp_memory_address(_log_timestamp)
-                _LOGGER.info(
-                    "_collect_energy_pulses for %s at %s not found, request counter from memory %s (from mem=%s, slot=%s, timestamp=%s)",
-                    self.mac,
-                    str(_log_timestamp),
-                    str(_mem_address),
-                    str(self._last_log_address),
-                    str(self._energy_last_populated_slot),
-                    str(self._energy_last_collected_timestamp),
-                )
-                self.request_energy_counters(_mem_address)
-                _energy_history_failed = True
-
-        # Validate all history values where present
-        if not _energy_history_failed:
-            return _energy_pulses
-        return None
-
-    def _update_energy_current_hour(self, _pulses_cur_hour):
-        """Update energy consumption (pulses) of current hour"""
-        _LOGGER.info(
-            "_update_energy_current_hour for %s | counter = %s, update= %s",
-            self.mac,
-            str(self._energy_pulses_current_hour),
-            str(_pulses_cur_hour),
-        )
-        _hour_rollover = False
-        if self._energy_pulses_current_hour is None:
-            self._energy_pulses_current_hour = _pulses_cur_hour
-            self.do_callback(FEATURE_POWER_CONSUMPTION_CURRENT_HOUR["id"])
-        else:
-            if self._energy_pulses_current_hour != _pulses_cur_hour:
-                if self._energy_pulses_current_hour > _pulses_cur_hour:
-                    _hour_rollover = True
-                self._energy_pulses_current_hour = _pulses_cur_hour
-                self.do_callback(FEATURE_POWER_CONSUMPTION_CURRENT_HOUR["id"])
-            # Update today
-            self._update_energy_today_now(_hour_rollover, False, False)
-
-    def _update_energy_today_now(
-        self, hour_rollover=False, history_rollover=False, day_rollover=False
-    ):
-        """Update energy consumption (pulses) of today up to now"""
-
-        _pulses_today_now = None
-
-        # Check for rollovers triggers
-        if hour_rollover and self._energy_rollover_hour_finished:
-            self._energy_rollover_hour_started = True
-            self._energy_rollover_hour_finished = False
-        if history_rollover and self._energy_rollover_history_finished:
-            self._energy_rollover_history_started = True
-            self._energy_rollover_history_finished = False
-        if day_rollover and self._energy_rollover_day_finished:
-            self._energy_rollover_day_started = True
-            self._energy_rollover_day_finished = False
-
-        # Set counter
-        if self._energy_rollover_hour_started:
-            if self._energy_rollover_history_started:
-                if self._energy_rollover_day_started:
-                    # Day rollover, reset to only current hour
-                    _pulses_today_now = self._energy_pulses_current_hour
-                    self._energy_rollover_day_started = False
-                    self._energy_rollover_day_finished = True
-                else:
-                    # Hour rollover, reset to hour history with current hour
-                    if (
-                        self._energy_pulses_today_hourly is None
-                        or self._energy_pulses_current_hour is None
-                    ):
-                        _pulses_today_now = None
-                    else:
-                        _pulses_today_now = (
-                            self._energy_pulses_today_hourly
-                            + self._energy_pulses_current_hour
-                        )
-                self._energy_rollover_hour_started = False
-                self._energy_rollover_hour_finished = True
-                self._energy_rollover_history_started = False
-                self._energy_rollover_history_finished = True
-            else:
-                # Wait for history_rollover, keep current counter
-                _pulses_today_now = None
-        else:
-            if self._energy_rollover_history_started:
-                # Wait for hour_rollover, keep current counter
-                _pulses_today_now = None
-            else:
-                # Regular update
-                if (
-                    self._energy_pulses_today_hourly is None
-                    or self._energy_pulses_current_hour is None
-                ):
-                    _pulses_today_now = None
-                else:
-                    _pulses_today_now = (
-                        self._energy_pulses_today_hourly
-                        + self._energy_pulses_current_hour
-                    )
-
-        if _pulses_today_now is None:
-            _LOGGER.info(
-                "_update_energy_today_now for %s | skip update, hour: %s=%s=%s, history: %s=%s=%s, day: %s=%s=%s",
-                self.mac,
-                str(hour_rollover),
-                str(self._energy_rollover_hour_started),
-                str(self._energy_rollover_hour_finished),
-                str(history_rollover),
-                str(self._energy_rollover_history_started),
-                str(self._energy_rollover_history_finished),
-                str(day_rollover),
-                str(self._energy_rollover_day_started),
-                str(self._energy_rollover_day_finished),
-            )
-        else:
-            _LOGGER.info(
-                "_update_energy_today_now for %s | counter = %s, update= %s (%s + %s)",
-                self.mac,
-                str(self._energy_pulses_today_now),
-                str(_pulses_today_now),
-                str(self._energy_pulses_today_hourly),
-                str(self._energy_pulses_current_hour),
-            )
-            if self._energy_pulses_today_now is None:
-                self._energy_pulses_today_now = _pulses_today_now
-                if self._energy_pulses_today_now is not None:
-                    self.do_callback(FEATURE_ENERGY_CONSUMPTION_TODAY["id"])
-            else:
-                if self._energy_pulses_today_now != _pulses_today_now:
-                    self._energy_pulses_today_now = _pulses_today_now
-                    self.do_callback(FEATURE_ENERGY_CONSUMPTION_TODAY["id"])
-
-    def _update_energy_previous_hour(self, prev_hour: datetime):
-        """Update energy consumption (pulses) of previous hour"""
-        _pulses_prev_hour = self._collect_energy_pulses(prev_hour, prev_hour)
-        _LOGGER.info(
-            "_update_energy_previous_hour for %s | counter = %s, update= %s, timestamp %s",
-            self.mac,
-            str(self._energy_pulses_yesterday),
-            str(_pulses_prev_hour),
-            str(prev_hour),
-        )
-        if self._energy_pulses_prev_hour is None:
-            self._energy_pulses_prev_hour = _pulses_prev_hour
-            if self._energy_pulses_prev_hour is not None:
-                self.do_callback(FEATURE_POWER_CONSUMPTION_PREVIOUS_HOUR["id"])
-        else:
-            if self._energy_pulses_prev_hour != _pulses_prev_hour:
-                self._energy_pulses_prev_hour = _pulses_prev_hour
-                self.do_callback(FEATURE_POWER_CONSUMPTION_PREVIOUS_HOUR["id"])
-
-    def _update_energy_yesterday(
-        self, start_yesterday: datetime, end_yesterday: datetime
-    ):
-        """Update energy consumption (pulses) of yesterday"""
-        _pulses_yesterday = self._collect_energy_pulses(start_yesterday, end_yesterday)
-        _LOGGER.debug(
-            "_update_energy_yesterday for %s | counter = %s, update= %s, range %s to %s",
-            self.mac,
-            str(self._energy_pulses_yesterday),
-            str(_pulses_yesterday),
-            str(start_yesterday),
-            str(end_yesterday),
-        )
-        if self._energy_pulses_yesterday is None:
-            self._energy_pulses_yesterday = _pulses_yesterday
-            if self._energy_pulses_yesterday is not None:
-                self.do_callback(FEATURE_POWER_CONSUMPTION_YESTERDAY["id"])
-        else:
-            if self._energy_pulses_yesterday != _pulses_yesterday:
-                self._energy_pulses_yesterday = _pulses_yesterday
-                self.do_callback(FEATURE_POWER_CONSUMPTION_YESTERDAY["id"])
-
-    def _update_energy_today_hourly(self, start_today: datetime, end_today: datetime):
-        """Update energy consumption (pulses) of today up to last hour"""
-        if start_today > end_today:
-            _pulses_today_hourly = 0
-        else:
-            _pulses_today_hourly = self._collect_energy_pulses(start_today, end_today)
-        _LOGGER.info(
-            "_update_energy_today_hourly for %s | counter = %s, update= %s, range %s to %s",
-            self.mac,
-            str(self._energy_pulses_today_hourly),
-            str(_pulses_today_hourly),
-            str(start_today),
-            str(end_today),
-        )
-        if self._energy_pulses_today_hourly is None:
-            self._energy_pulses_today_hourly = _pulses_today_hourly
-            if self._energy_pulses_today_hourly is not None:
-                self.do_callback(FEATURE_POWER_CONSUMPTION_TODAY["id"])
-        else:
-            if self._energy_pulses_today_hourly != _pulses_today_hourly:
-                self._energy_pulses_today_hourly = _pulses_today_hourly
-                self.do_callback(FEATURE_POWER_CONSUMPTION_TODAY["id"])
-
-    def request_energy_counters(self, log_address=None, callback=None):
-        """Request power log of specified address"""
-        _LOGGER.debug(
-            "request_energy_counters for %s of address %s", self.mac, str(log_address)
-        )
-        if log_address is None:
-            log_address = self._last_log_address
-        if log_address is not None:
-            if self._energy_history_collecting and (
-                self._energy_history_collecting_timestamp
-                < datetime.now() - timedelta(minutes=15)
-            ):
-                _LOGGER.debug(
-                    "Skip request_energy_counters for %s of address %s",
-                    self.mac,
-                    str(log_address),
-                )
-            elif len(self._energy_history) > 48:
-                # Energy history already collected
-                if (
-                    log_address == self._last_log_address
-                    and self._energy_last_populated_slot == 4
-                ):
-                    # Rollover of energy counter slot, get new memory address first
-                    self._energy_last_populated_slot = 0
-                    self._request_info(self.request_energy_counters)
-                else:
-                    # Request new energy counters
-                    if self._energy_memory.get(log_address, 0) < 4:
-                        self.message_sender(
-                            CircleEnergyCountersRequest(self._mac, log_address),
-                            None,
-                            0,
-                            PRIORITY_LOW,
-                        )
-                    else:
-                        _LOGGER.info(
-                            "Drop known request_energy_counters for %s of address %s",
-                            self.mac,
-                            str(log_address),
-                        )
-            else:
-                # Collect energy counters of today and yesterday
-                # Each request contains will return 4 hours, except last request
-
-                # TODO: validate range of log_addresses
-                self._energy_history_collecting = True
-                self._energy_history_collecting_timestamp = datetime.now()
-                for req_log_address in range(log_address - 13, log_address):
-                    if self._energy_memory.get(req_log_address, 0) < 4:
-                        self.message_sender(
-                            CircleEnergyCountersRequest(self._mac, req_log_address),
-                            None,
-                            0,
-                            PRIORITY_LOW,
-                        )
-                    else:
-                        _LOGGER.info(
-                            "Drop known request_energy_counters at collecting for %s of address %s",
-                            self.mac,
-                            str(log_address),
-                        )
-                if self._energy_memory.get(log_address, 0) < 4:
-                    self.message_sender(
-                        CircleEnergyCountersRequest(self._mac, log_address),
-                        callback,
-                        0,
-                        PRIORITY_LOW,
-                    )
-                else:
-                    _LOGGER.info(
-                        "Drop known request_energy_counters at collecting end for %s of address %s",
-                        self.mac,
-                        str(log_address),
-                    )
-
-    def _response_energy_counters(self, message: CircleEnergyCountersResponse):
-        """
-        Save historical energy information in local counters
-        Each response message contains 4 log counters (slots)
-        of the energy pulses collected during the previous hour of given timestamp
-        """
-        if message.logaddr.value == self._last_log_address:
-            self._energy_last_populated_slot = 0
-
-        # Collect energy history pulses from received log address
-        # Store pulse in self._energy_history using the timestamp in UTC as index
-        _utc_hour_timestamp = datetime.utcnow().replace(
-            minute=0, second=0, microsecond=0
-        )
-        _local_midnight_timestamp = datetime.now().replace(
-            hour=0, minute=0, second=0, microsecond=0
-        )
-        _local_hour = datetime.now().hour
-        _utc_midnight_timestamp = _utc_hour_timestamp - timedelta(hours=_local_hour)
-        _midnight_rollover = False
-        _history_rollover = False
-
-        for _slot in range(1, 5):
-            if (
-                _log_timestamp := getattr(message, "logdate%d" % (_slot,)).value
-            ) is None:
-                break
-            # Register collected history memory
-            if _slot > self._energy_memory.get(message.logaddr.value, 0):
-                self._energy_memory[message.logaddr.value] = _slot
-
-            self._energy_history[_log_timestamp] = getattr(
-                message, "pulses%d" % (_slot,)
-            ).value
-
-            # Store last populated _slot
-            if message.logaddr.value == self._last_log_address:
-                self._energy_last_populated_slot = _slot
-
-            # Store most recent timestamp of collected pulses
-            self._energy_last_collected_timestamp = max(
-                self._energy_last_collected_timestamp, _log_timestamp
-            )
-
-            # Trigger history rollover
-            if (
-                _log_timestamp == _utc_hour_timestamp
-                and self._energy_last_rollover_timestamp < _utc_hour_timestamp
-            ):
-                self._energy_last_rollover_timestamp = _utc_hour_timestamp
-                _history_rollover = True
-                _LOGGER.info(
-                    "_response_energy_counters for %s | history rollover, reset date to %s",
-                    self.mac,
-                    str(_utc_hour_timestamp),
-                )
-
-            # Trigger midnight rollover
-            if (
-                _log_timestamp == _utc_midnight_timestamp
-                and self._energy_consumption_today_reset < _local_midnight_timestamp
-            ):
-                _LOGGER.info(
-                    "_response_energy_counters for %s | midnight rollover, reset date to %s",
-                    self.mac,
-                    str(_local_midnight_timestamp),
-                )
-                self._energy_consumption_today_reset = _local_midnight_timestamp
-                _midnight_rollover = True
-
-        # Reset energy collection progress
-        if (
-            self._energy_history_collecting
-            and len(self._energy_history) > 48
-            and self._energy_last_collected_timestamp == _utc_hour_timestamp
-        ):
-            self._energy_last_rollover_timestamp = self._energy_last_collected_timestamp
-            self._energy_history_collecting = False
-            _history_rollover = False
-            _midnight_rollover = False
-        else:
-            _LOGGER.info(
-                "_response_energy_counters for %s | collection not running, len=%s, timestamp:%s=%s",
-                self.mac,
-                str(len(self._energy_history)),
-                str(self._energy_last_collected_timestamp),
-                str(_utc_hour_timestamp),
-            )
-
-        # Update energy counters
-        if not self._energy_history_collecting:
-            self._update_energy_previous_hour(_utc_hour_timestamp)
-            self._update_energy_today_hourly(
-                _utc_midnight_timestamp + timedelta(hours=1),
-                _utc_hour_timestamp,
-            )
-            self._update_energy_yesterday(
-                _utc_midnight_timestamp - timedelta(hours=23),
-                _utc_midnight_timestamp,
-            )
-            self._update_energy_today_now(False, _history_rollover, _midnight_rollover)
-        else:
-            logstring = (
-                "_response_energy_counters for %s | self._energy_history_collecting running %s"
-                % (self.mac, str(_local_midnight_timestamp))
-            )
-            _LOGGER.info(logstring)
-
-        # Cleanup energy history for more than 8 day's ago
-        _8_days_ago = datetime.utcnow().replace(
-            minute=0, second=0, microsecond=0
-        ) - timedelta(days=8)
-        for log_timestamp in list(self._energy_history.keys()):
-            if log_timestamp < _8_days_ago:
-                del self._energy_history[log_timestamp]
-
-    def _response_clock(self, message: CircleClockResponse):
-        log_date = datetime(
-            datetime.now().year,
-            datetime.now().month,
-            datetime.now().day,
-            message.time.value.hour,
-            message.time.value.minute,
-            message.time.value.second,
-        )
-        clock_offset = message.timestamp.replace(microsecond=0) - (
-            log_date + self.timezone_delta
-        )
-        if clock_offset.days == -1:
-            self._clock_offset = clock_offset.seconds - 86400
-        else:
-            self._clock_offset = clock_offset.seconds
-        _LOGGER.debug(
-            "Clock of node %s has drifted %s sec",
-            self.mac,
-            str(self._clock_offset),
-        )
-
-    def get_clock(self, callback=None):
-        """get current datetime of internal clock of Circle."""
-        self.message_sender(
-            CircleClockGetRequest(self._mac),
-            callback,
-            0,
-            PRIORITY_LOW,
-        )
-
-    def set_clock(self, callback=None):
-        """set internal clock of CirclePlus."""
-        self.message_sender(
-            CircleClockSetRequest(self._mac, datetime.utcnow()),
-            callback,
-        )
-
-    def sync_clock(self, max_drift=0):
-        """Resync clock of node if time has drifted more than MAX_TIME_DRIFT"""
-        if self._clock_offset is not None:
-            if max_drift == 0:
-                max_drift = MAX_TIME_DRIFT
-            if (self._clock_offset > max_drift) or (self._clock_offset < -(max_drift)):
-                _LOGGER.info(
-                    "Reset clock of node %s because time has drifted %s sec",
-                    self.mac,
-                    str(self._clock_offset),
-                )
-                self.set_clock()
-
-    def _energy_timestamp_memory_address(self, utc_timestamp: datetime):
-        """Return memory address for given energy counter timestamp"""
-        _utc_now_timestamp = datetime.utcnow().replace(
-            minute=0, second=0, microsecond=0
-        )
-        if utc_timestamp > _utc_now_timestamp:
-            return None
-
-        _seconds_offset = (_utc_now_timestamp - utc_timestamp).seconds
-        _hours_offset = _seconds_offset / 3600
-
-        if (_slot := self._energy_last_populated_slot) == 0:
-            _slot = 4
-        _address = self._last_log_address
-
-        # last known
-        _hours = 1
-        while _hours <= _hours_offset:
-            _slot -= 1
-            if _slot == 0:
-                _address -= 1
-                _slot = 4
-            _hours += 1
-
-        return _address
diff --git a/plugwise/stick/nodes/circle_plus.py b/plugwise/stick/nodes/circle_plus.py
deleted file mode 100644
index e260be467..000000000
--- a/plugwise/stick/nodes/circle_plus.py
+++ /dev/null
@@ -1,139 +0,0 @@
-"""Plugwise Circle+ node object."""
-from datetime import datetime
-import logging
-
-from ..constants import MAX_TIME_DRIFT, PRIORITY_LOW, UTF8_DECODE
-from ..messages.requests import (
-    CirclePlusRealTimeClockGetRequest,
-    CirclePlusRealTimeClockSetRequest,
-    CirclePlusScanRequest,
-)
-from ..messages.responses import CirclePlusRealTimeClockResponse, CirclePlusScanResponse
-from ..nodes.circle import PlugwiseCircle
-
-_LOGGER = logging.getLogger(__name__)
-
-
-class PlugwiseCirclePlus(PlugwiseCircle):
-    """provides interface to the Plugwise Circle+ nodes"""
-
-    def __init__(self, mac, address, message_sender):
-        super().__init__(mac, address, message_sender)
-        self._plugwise_nodes = {}
-        self._scan_response = {}
-        self._scan_for_nodes_callback = None
-        self._realtime_clock_offset = None
-        self.get_real_time_clock(self.sync_realtime_clock)
-
-    def message_for_circle_plus(self, message):
-        """
-        Process received message
-        """
-        if isinstance(message, CirclePlusRealTimeClockResponse):
-            self._response_realtime_clock(message)
-        elif isinstance(message, CirclePlusScanResponse):
-            self._process_scan_response(message)
-        else:
-            _LOGGER.waning(
-                "Unsupported message type '%s' received from circle with mac %s",
-                str(message.__class__.__name__),
-                self.mac,
-            )
-
-    def scan_for_nodes(self, callback=None):
-        """Scan for registered nodes."""
-        self._scan_for_nodes_callback = callback
-        for node_address in range(0, 64):
-            self.message_sender(CirclePlusScanRequest(self._mac, node_address))
-            self._scan_response[node_address] = False
-
-    def _process_scan_response(self, message):
-        """Process scan response message."""
-        _LOGGER.debug(
-            "Process scan response for address %s", message.node_address.value
-        )
-        if message.node_mac.value != b"FFFFFFFFFFFFFFFF":
-            _LOGGER.debug(
-                "Linked plugwise node with mac %s found",
-                message.node_mac.value.decode(UTF8_DECODE),
-            )
-            #  TODO: 20220206 is there 'mac' in the dict? Otherwise it can be rewritten to just if message... in
-            if not self._plugwise_nodes.get(message.node_mac.value.decode(UTF8_DECODE)):
-                self._plugwise_nodes[
-                    message.node_mac.value.decode(UTF8_DECODE)
-                ] = message.node_address.value
-        if self._scan_for_nodes_callback:
-            # Check if scan is complete before execute callback
-            scan_complete = False
-            self._scan_response[message.node_address.value] = True
-            for node_address in range(0, 64):
-                if not self._scan_response[node_address]:
-                    if node_address < message.node_address.value:
-                        # Apparently missed response so send new scan request if it's not in queue yet
-                        _LOGGER.debug(
-                            "Resend missing scan request for address %s",
-                            str(node_address),
-                        )
-                        self.message_sender(
-                            CirclePlusScanRequest(self._mac, node_address)
-                        )
-                    break
-                if node_address == 63:
-                    scan_complete = True
-            if scan_complete and self._scan_for_nodes_callback:
-                self._scan_for_nodes_callback(self._plugwise_nodes)
-                self._scan_for_nodes_callback = None
-                self._plugwise_nodes = {}
-
-    def get_real_time_clock(self, callback=None):
-        """get current datetime of internal clock of CirclePlus."""
-        self.message_sender(
-            CirclePlusRealTimeClockGetRequest(self._mac),
-            callback,
-            0,
-            PRIORITY_LOW,
-        )
-
-    def _response_realtime_clock(self, message):
-        realtime_clock_dt = datetime(
-            datetime.now().year,
-            datetime.now().month,
-            datetime.now().day,
-            message.time.value.hour,
-            message.time.value.minute,
-            message.time.value.second,
-        )
-        realtime_clock_offset = message.timestamp.replace(microsecond=0) - (
-            realtime_clock_dt + self.timezone_delta
-        )
-        if realtime_clock_offset.days == -1:
-            self._realtime_clock_offset = realtime_clock_offset.seconds - 86400
-        else:
-            self._realtime_clock_offset = realtime_clock_offset.seconds
-        _LOGGER.debug(
-            "Realtime clock of node %s has drifted %s sec",
-            self.mac,
-            str(self._clock_offset),
-        )
-
-    def set_real_time_clock(self, callback=None):
-        """set internal clock of CirclePlus."""
-        self.message_sender(
-            CirclePlusRealTimeClockSetRequest(self._mac, datetime.utcnow()),
-            callback,
-        )
-
-    def sync_realtime_clock(self, max_drift=0):
-        """Sync real time clock of node if time has drifted more than max drifted."""
-        if self._realtime_clock_offset is not None:
-            if max_drift == 0:
-                max_drift = MAX_TIME_DRIFT
-            if (self._realtime_clock_offset > max_drift) or (
-                self._realtime_clock_offset < -(max_drift)
-            ):
-                _LOGGER.info(
-                    "Reset realtime clock of node %s because time has drifted %s sec",
-                    self.mac,
-                    str(self._clock_offset),
-                )
-                self.set_real_time_clock()
diff --git a/plugwise/stick/nodes/scan.py b/plugwise/stick/nodes/scan.py
deleted file mode 100644
index 32ee88093..000000000
--- a/plugwise/stick/nodes/scan.py
+++ /dev/null
@@ -1,139 +0,0 @@
-"""Plugwise Scan node object."""
-import logging
-
-from ..constants import (
-    FEATURE_MOTION,
-    FEATURE_PING,
-    FEATURE_RSSI_IN,
-    FEATURE_RSSI_OUT,
-    SCAN_CONFIGURE_ACCEPTED,
-    SCAN_DAYLIGHT_MODE,
-    SCAN_MOTION_RESET_TIMER,
-    SCAN_SENSITIVITY_HIGH,
-    SCAN_SENSITIVITY_MEDIUM,
-    SCAN_SENSITIVITY_OFF,
-)
-from ..messages.requests import ScanConfigureRequest, ScanLightCalibrateRequest
-from ..messages.responses import NodeAckResponse, NodeSwitchGroupResponse
-from ..nodes.sed import NodeSED
-
-_LOGGER = logging.getLogger(__name__)
-
-
-class PlugwiseScan(NodeSED):
-    """provides interface to the Plugwise Scan nodes"""
-
-    def __init__(self, mac, address, message_sender):
-        super().__init__(mac, address, message_sender)
-        self._features = (
-            FEATURE_MOTION["id"],
-            FEATURE_PING["id"],
-            FEATURE_RSSI_IN["id"],
-            FEATURE_RSSI_OUT["id"],
-        )
-        self._motion_state = False
-        self._motion_reset_timer = None
-        self._daylight_mode = None
-        self._sensitivity = None
-        self._new_motion_reset_timer = None
-        self._new_daylight_mode = None
-        self._new_sensitivity = None
-
-    @property
-    def motion(self) -> bool:
-        """Return the last known motion state"""
-        return self._motion_state
-
-    def message_for_scan(self, message):
-        """
-        Process received message
-        """
-        if isinstance(message, NodeSwitchGroupResponse):
-            _LOGGER.debug(
-                "Switch group %s to state %s received from %s",
-                str(message.group.value),
-                str(message.power_state.value),
-                self.mac,
-            )
-            self._process_switch_group(message)
-        elif isinstance(message, NodeAckResponse):
-            self._process_ack_message(message)
-        else:
-            _LOGGER.info(
-                "Unsupported message %s received from %s",
-                message.__class__.__name__,
-                self.mac,
-            )
-
-    def _process_ack_message(self, message):
-        """Process acknowledge message"""
-        if message.ack_id == SCAN_CONFIGURE_ACCEPTED:
-            self._motion_reset_timer = self._new_motion_reset_timer
-            self._daylight_mode = self._new_daylight_mode
-            self._sensitivity = self._new_sensitivity
-        else:
-            _LOGGER.info(
-                "Unsupported ack message %s received for %s",
-                str(message.ack_id),
-                self.mac,
-            )
-
-    def _process_switch_group(self, message):
-        """Switch group request from Scan"""
-        if message.power_state.value == 0:
-            # turn off => clear motion
-            if self._motion_state:
-                self._motion_state = False
-                self.do_callback(FEATURE_MOTION["id"])
-        elif message.power_state.value == 1:
-            # turn on => motion
-            if not self._motion_state:
-                self._motion_state = True
-                self.do_callback(FEATURE_MOTION["id"])
-        else:
-            _LOGGER.warning(
-                "Unknown power_state (%s) received from %s",
-                str(message.power_state.value),
-                self.mac,
-            )
-
-    #  TODO: 20220125 snakestyle name
-    #  pylint: disable=invalid-name
-    def CalibrateLight(self, callback=None):
-        """Queue request to calibration light sensitivity"""
-        self._queue_request(ScanLightCalibrateRequest(self._mac), callback)
-
-    #  TODO: 20220125 snakestyle name
-    #  pylint: disable=invalid-name
-    def Configure_scan(
-        self,
-        motion_reset_timer=SCAN_MOTION_RESET_TIMER,
-        sensitivity_level=SCAN_SENSITIVITY_MEDIUM,
-        daylight_mode=SCAN_DAYLIGHT_MODE,
-        callback=None,
-    ):
-        """Queue request to set motion reporting settings"""
-        self._new_motion_reset_timer = motion_reset_timer
-        self._new_daylight_mode = daylight_mode
-        if sensitivity_level == SCAN_SENSITIVITY_HIGH:
-            sensitivity_value = 20  # b'14'
-        elif sensitivity_level == SCAN_SENSITIVITY_OFF:
-            sensitivity_value = 255  # b'FF'
-        else:
-            # Default to medium:
-            sensitivity_value = 30  # b'1E'
-        self._new_sensitivity = sensitivity_level
-        self._queue_request(
-            ScanConfigureRequest(
-                self._mac, motion_reset_timer, sensitivity_value, daylight_mode
-            ),
-            callback,
-        )
-
-    #  TODO: 20220125 snakestyle name
-    #  pylint: disable=invalid-name
-    def SetMotionAction(self, callback=None):
-        """Queue Configure Scan to signal motion"""
-        # TODO:
-
-        # self._queue_request(NodeSwitchGroupRequest(self._mac), callback)
diff --git a/plugwise/stick/nodes/sed.py b/plugwise/stick/nodes/sed.py
deleted file mode 100644
index c14f9b365..000000000
--- a/plugwise/stick/nodes/sed.py
+++ /dev/null
@@ -1,164 +0,0 @@
-"""Plugwise SED (Sleeping Endpoint Device) base object."""
-
-# TODO:
-# - Expose awake state as sensor
-# - Set available state after 2 missed awake messages
-
-import logging
-
-from ..constants import (
-    FEATURE_PING,
-    FEATURE_RSSI_IN,
-    FEATURE_RSSI_OUT,
-    PRIORITY_HIGH,
-    SED_AWAKE_BUTTON,
-    SED_AWAKE_FIRST,
-    SED_AWAKE_MAINTENANCE,
-    SED_AWAKE_STARTUP,
-    SED_AWAKE_STATE,
-    SED_CLOCK_INTERVAL,
-    SED_CLOCK_SYNC,
-    SED_MAINTENANCE_INTERVAL,
-    SED_SLEEP_FOR,
-    SED_STAY_ACTIVE,
-    SLEEP_SET,
-)
-from ..messages.requests import NodeInfoRequest, NodePingRequest, NodeSleepConfigRequest
-from ..messages.responses import NodeAckLargeResponse, NodeAwakeResponse
-from ..nodes import PlugwiseNode
-
-_LOGGER = logging.getLogger(__name__)
-
-
-class NodeSED(PlugwiseNode):
-    """provides base class for SED based nodes like Scan, Sense & Switch"""
-
-    def __init__(self, mac, address, message_sender):
-        super().__init__(mac, address, message_sender)
-        self._sed_requests = {}
-        self.maintenance_interval = SED_MAINTENANCE_INTERVAL
-        self._new_maintenance_interval = None
-        self._wake_up_interval = None
-        self._battery_powered = True
-
-    def message_for_sed(self, message):
-        """
-        Process received message
-        """
-        if isinstance(message, NodeAwakeResponse):
-            self._process_awake_response(message)
-        elif isinstance(message, NodeAckLargeResponse):
-            if message.ack_id == SLEEP_SET:
-                self.maintenance_interval = self._new_maintenance_interval
-            else:
-                self.message_for_scan(message)
-                self.message_for_switch(message)
-                self.message_for_sense(message)
-        else:
-            self.message_for_scan(message)
-            self.message_for_switch(message)
-            self.message_for_sense(message)
-
-    def message_for_scan(self, message):
-        """Pass messages to PlugwiseScan class"""
-
-    def message_for_switch(self, message):
-        """Pass messages to PlugwiseSwitch class"""
-
-    def message_for_sense(self, message):
-        """Pass messages to PlugwiseSense class"""
-
-    def _process_awake_response(self, message):
-        """ "Process awake message"""
-        _LOGGER.debug(
-            "Awake message type '%s' received from %s",
-            str(message.awake_type.value),
-            self.mac,
-        )
-        if message.awake_type.value in [
-            SED_AWAKE_MAINTENANCE,
-            SED_AWAKE_FIRST,
-            SED_AWAKE_STARTUP,
-            SED_AWAKE_BUTTON,
-        ]:
-            for pending_request in self._sed_requests.values():
-                request_message, callback = pending_request
-                _LOGGER.info(
-                    "Send queued %s message to SED node %s",
-                    request_message.__class__.__name__,
-                    self.mac,
-                )
-                self.message_sender(request_message, callback, -1, PRIORITY_HIGH)
-            self._sed_requests = {}
-        else:
-            if message.awake_type.value == SED_AWAKE_STATE:
-                _LOGGER.debug("Node %s awake for state change", self.mac)
-            else:
-                _LOGGER.info(
-                    "Unknown awake message type (%s) received for node %s",
-                    str(message.awake_type.value),
-                    self.mac,
-                )
-
-    def _queue_request(self, request_message, callback=None):
-        """Queue request to be sent when SED is awake. Last message wins."""
-        self._sed_requests[request_message.ID] = (
-            request_message,
-            callback,
-        )
-
-    def _request_info(self, callback=None):
-        """Request info from node"""
-        self._queue_request(
-            NodeInfoRequest(self._mac),
-            callback,
-        )
-
-    def _request_ping(self, callback=None, ignore_sensor=True):
-        """Ping node"""
-        if (
-            ignore_sensor
-            or self._callbacks.get(FEATURE_PING["id"])
-            or self._callbacks.get(FEATURE_RSSI_IN["id"])
-            or self._callbacks.get(FEATURE_RSSI_OUT["id"])
-        ):
-            self._queue_request(
-                NodePingRequest(self._mac),
-                callback,
-            )
-        else:
-            _LOGGER.debug(
-                "Drop ping request for SED %s because no callback is registered",
-                self.mac,
-            )
-
-    def _wake_up_interval_accepted(self):
-        """Callback after wake up interval is received and accepted by SED."""
-        self._wake_up_interval = self._new_maintenance_interval
-
-    #  TODO: 20220125 snakestyle name
-    #  pylint: disable=invalid-name
-    def Configure_SED(
-        self,
-        stay_active=SED_STAY_ACTIVE,
-        sleep_for=SED_SLEEP_FOR,
-        maintenance_interval=SED_MAINTENANCE_INTERVAL,
-        clock_sync=SED_CLOCK_SYNC,
-        clock_interval=SED_CLOCK_INTERVAL,
-    ):
-        """Reconfigure the sleep/awake settings for a SED send at next awake of SED"""
-        message = NodeSleepConfigRequest(
-            self._mac,
-            stay_active,
-            maintenance_interval,
-            sleep_for,
-            clock_sync,
-            clock_interval,
-        )
-        self._queue_request(message, self._wake_up_interval_accepted)
-        self._new_maintenance_interval = maintenance_interval
-        _LOGGER.info(
-            "Queue %s message to be send at next awake of SED node %s",
-            message.__class__.__name__,
-            self.mac,
-        )
diff --git a/plugwise/stick/nodes/sense.py b/plugwise/stick/nodes/sense.py
deleted file mode 100644
index af6574f62..000000000
--- a/plugwise/stick/nodes/sense.py
+++ /dev/null
@@ -1,86 +0,0 @@
-"""Plugwise Sense node object."""
-import logging
-
-from ..constants import (
-    FEATURE_HUMIDITY,
-    FEATURE_PING,
-    FEATURE_RSSI_IN,
-    FEATURE_RSSI_OUT,
-    FEATURE_TEMPERATURE,
-    SENSE_HUMIDITY_MULTIPLIER,
-    SENSE_HUMIDITY_OFFSET,
-    SENSE_TEMPERATURE_MULTIPLIER,
-    SENSE_TEMPERATURE_OFFSET,
-)
-from ..messages.responses import SenseReportResponse
-from ..nodes.sed import NodeSED
-
-_LOGGER = logging.getLogger(__name__)
-
-
-class PlugwiseSense(NodeSED):
-    """provides interface to the Plugwise Sense nodes"""
-
-    def __init__(self, mac, address, message_sender):
-        super().__init__(mac, address, message_sender)
-        self._features = (
-            FEATURE_HUMIDITY["id"],
-            FEATURE_PING["id"],
-            FEATURE_RSSI_IN["id"],
-            FEATURE_RSSI_OUT["id"],
-            FEATURE_TEMPERATURE["id"],
-        )
-        self._temperature = None
-        self._humidity = None
-
-    @property
-    def humidity(self) -> int:
-        """Return the current humidity."""
-        return self._humidity
-
-    @property
-    def temperature(self) -> int:
-        """Return the current temperature."""
-        return self._temperature
-
-    def message_for_sense(self, message):
-        """
-        Process received message
-        """
-        if isinstance(message, SenseReportResponse):
-            self._process_sense_report(message)
-        else:
-            _LOGGER.info(
-                "Unsupported message %s received from %s",
-                message.__class__.__name__,
-                self.mac,
-            )
-
-    def _process_sense_report(self, message):
-        """process sense report message to extract current temperature and humidity values."""
-        if message.temperature.value != 65535:
-            new_temperature = int(
-                SENSE_TEMPERATURE_MULTIPLIER * (message.temperature.value / 65536)
-                - SENSE_TEMPERATURE_OFFSET
-            )
-            if self._temperature != new_temperature:
-                self._temperature = new_temperature
-                _LOGGER.debug(
-                    "Sense report received from %s with new temperature level of %s",
-                    self.mac,
-                    str(self._temperature),
-                )
-                self.do_callback(FEATURE_TEMPERATURE["id"])
-        if message.humidity.value != 65535:
-            new_humidity = int(
-                SENSE_HUMIDITY_MULTIPLIER * (message.humidity.value / 65536)
-                - SENSE_HUMIDITY_OFFSET
-            )
-            if self._humidity != new_humidity:
-                self._humidity = new_humidity
-                _LOGGER.debug(
-                    "Sense report received from %s with new humidity level of %s",
-                    self.mac,
-                    str(self._humidity),
-                )
-                self.do_callback(FEATURE_HUMIDITY["id"])
diff --git a/plugwise/stick/nodes/stealth.py b/plugwise/stick/nodes/stealth.py
deleted file mode 100644
index 102e309c8..000000000
--- a/plugwise/stick/nodes/stealth.py
+++ /dev/null
@@ -1,6 +0,0 @@
-"""Plugwise Stealth node object."""
-from ..nodes.circle import PlugwiseCircle
-
-
-class PlugwiseStealth(PlugwiseCircle):
-    """provides interface to the Plugwise Stealth nodes"""
diff --git a/plugwise/stick/nodes/switch.py b/plugwise/stick/nodes/switch.py
deleted file mode 100644
index f55f0c605..000000000
--- a/plugwise/stick/nodes/switch.py
+++ /dev/null
@@ -1,59 +0,0 @@
-"""Plugwise switch node object."""
-import logging
-
-from ..constants import FEATURE_PING, FEATURE_RSSI_IN, FEATURE_RSSI_OUT, FEATURE_SWITCH
-from ..messages.responses import NodeSwitchGroupResponse
-from ..nodes.sed import NodeSED
-
-_LOGGER = logging.getLogger(__name__)
-
-
-class PlugwiseSwitch(NodeSED):
-    """provides interface to the Plugwise Switch nodes"""
-
-    def __init__(self, mac, address, message_sender):
-        super().__init__(mac, address, message_sender)
-        self._features = (
-            FEATURE_PING["id"],
-            FEATURE_RSSI_IN["id"],
-            FEATURE_RSSI_OUT["id"],
-            FEATURE_SWITCH["id"],
-        )
-        self._switch_state = False
-
-    @property
-    def switch(self) -> bool:
-        """Return the last known switch state"""
-        return self._switch_state
-
-    def message_for_switch(self, message):
-        """
-        Process received message
-        """
-        if isinstance(message, NodeSwitchGroupResponse):
-            _LOGGER.debug(
-                "Switch group request %s received from %s for group id %s",
-                str(message.power_state),
-                self.mac,
-                str(message.group),
-            )
-            self._process_switch_group(message)
-
-    def _process_switch_group(self, message):
-        """Switch group request from Scan"""
-        if message.power_state == 0:
-            # turn off => clear motion
-            if self._switch_state:
-                self._switch_state = False
-                self.do_callback(FEATURE_SWITCH["id"])
-        elif message.power_state == 1:
-            # turn on => motion
-            if not self._switch_state:
-                self._switch_state = True
-                self.do_callback(FEATURE_SWITCH["id"])
-        else:
-            _LOGGER.debug(
-                "Unknown power_state (%s) received from %s",
-                str(message.power_state),
-                self.mac,
-            )
diff --git a/plugwise/stick/parser.py b/plugwise/stick/parser.py
deleted file mode 100644
index 19a6b0733..000000000
--- a/plugwise/stick/parser.py
+++ /dev/null
@@ -1,144 +0,0 @@
-"""
-Data parser for USB-Stick
-
-The parser will:
-- buffer receiving data
-- filter out received zigbee routing data
-- collect message data by detecting header and footer
-- detect message type based on message ID or fixed sequence ID
-- validate received data on checksum
-- decode collected data into a response message instance
-- pass over received messages to message_processor (controller.py)
-
-"""
-
-import logging
-
-from .constants import MESSAGE_FOOTER, MESSAGE_HEADER
-from .exceptions import (
-    InvalidMessageChecksum,
-    InvalidMessageFooter,
-    InvalidMessageHeader,
-    InvalidMessageLength,
-)
-from .messages.responses import get_message_response
-
-_LOGGER = logging.getLogger(__name__)
-
-
-class PlugwiseParser:
-    """Transform Plugwise message from wire format to response message object."""
-
-    def __init__(self, message_processor):
-        self.message_processor = message_processor
-        self._buffer = bytes([])
-        self._parsing = False
-        self._message = None
-
-    def feed(self, data):
-        """
-        Add new incoming data to buffer and try to process
-        """
-        _LOGGER.debug("Feed data: %s", str(data))
-        self._buffer += data
-        if len(self._buffer) >= 8:
-            if not self._parsing:
-                self.parse_data()
-
-    def next_message(self, message):
-        """
-        Process next packet if present
-        """
-        try:
-            self.message_processor(message)
-        # TODO: narrow exception
-        except Exception as err:  # pylint: disable=broad-except
-            _LOGGER.error(
-                "Error while processing %s : %s",
-                self._message.__class__.__name__,
-                err,
-            )
-            _LOGGER.error(err, exc_info=True)
-
-    def parse_data(self):
-        """
-        Process next set of packet data
-        """
-        _LOGGER.debug("Parse data: %s ", str(self._buffer))
-        if not self._parsing:
-            self._parsing = True
-
-            # Lookup header of message in buffer
-            _LOGGER.debug(
-                "Lookup message header (%s) in (%s)",
-                str(MESSAGE_HEADER),
-                str(self._buffer),
-            )
-            if (header_index := self._buffer.find(MESSAGE_HEADER)) == -1:
-                _LOGGER.debug("No valid message header found yet")
-            else:
-                _LOGGER.debug(
-                    "Valid message header found at index %s", str(header_index)
-                )
-                self._buffer = self._buffer[header_index:]
-
-                # Header available, lookup footer of message in buffer
-                _LOGGER.debug(
-                    "Lookup message footer (%s) in (%s)",
-                    str(MESSAGE_FOOTER),
-                    str(self._buffer),
-                )
-                if (footer_index := self._buffer.find(MESSAGE_FOOTER)) == -1:
-                    _LOGGER.debug("No valid message footer found yet")
-                else:
-                    _LOGGER.debug(
-                        "Valid message footer found at index %s", str(footer_index)
-                    )
-                    self._message = get_message_response(
-                        self._buffer[4:8], footer_index, self._buffer[8:12]
-                    )
-                    if self._message:
-                        try:
-                            self._message.deserialize(self._buffer[: footer_index + 2])
-                        except (
-                            InvalidMessageChecksum,
-                            InvalidMessageFooter,
-                            InvalidMessageHeader,
-                            InvalidMessageLength,
-                        ) as err:
-                            _LOGGER.warning(err)
-                        # TODO: narrow exception
-                        except Exception as err:  # pylint: disable=broad-except
-                            _LOGGER.error(
-                                "Failed to parse %s message (%s)",
-                                self._message.__class__.__name__,
-                                str(self._buffer[: footer_index + 2]),
-                            )
-                            _LOGGER.error(err)
-                        else:
-                            # Submit message
-                            self.next_message(self._message)
-                        # Parse remaining buffer
-                        self.reset_parser(self._buffer[footer_index + 2 :])
-                    else:
-                        # skip this message, so remove header from buffer
-                        _LOGGER.error(
-                            "Skip unknown message %s",
-                            str(self._buffer[: footer_index + 2]),
-                        )
-                        self.reset_parser(self._buffer[6:])
-            self._parsing = False
-        else:
-            _LOGGER.debug("Skip parsing session")
-
-    def reset_parser(self, new_buffer=bytes([])):
-        _LOGGER.debug("Reset parser : %s", new_buffer)
-        if new_buffer == b"\x83":
-            # Skip additional byte sometimes appended after footer
-            self._buffer = bytes([])
-        else:
-            self._buffer = new_buffer
-        self._message = None
-        self._parsing = False
-        if len(self._buffer) > 0:
-            self.parse_data()
diff --git a/plugwise/stick/util.py b/plugwise/stick/util.py
deleted file mode 100644
index 46c8820e5..000000000
--- a/plugwise/stick/util.py
+++ /dev/null
@@ -1,291 +0,0 @@
-"""
-Use of this source code is governed by the MIT license found in the LICENSE file.
-
-Plugwise protocol helpers
-"""
-from __future__ import annotations
-
-import binascii
-import datetime
-import re
-import struct
-
-import crcmod
-
-from .constants import HW_MODELS, LOGADDR_OFFSET, PLUGWISE_EPOCH, UTF8_DECODE
-
-crc_fun = crcmod.mkCrcFun(0x11021, rev=False, initCrc=0x0000, xorOut=0x0000)
-
-
-# NOTE: this function version_to_model is shared between Smile and USB
-def version_to_model(version: str | None) -> str | None:
-    """Translate hardware_version to device type."""
-    if version is None:
-        return None
-
-    model = HW_MODELS.get(version)
-    if model is None:
-        model = HW_MODELS.get(version[4:10])
-    if model is None:
-        # Try again with reversed order
-        model = HW_MODELS.get(version[-2:] + version[-4:-2] + version[-6:-4])
-
-    return model if model is not None else "Unknown"
-
-
-def validate_mac(mac: str) -> bool:
-    if not re.match("^[A-F0-9]+$", mac):
-        return False
-    try:
-        _ = int(mac, 16)
-    except ValueError:
-        return False
-    return True
-
-
-def inc_seq_id(seq_id: str | None, value: int = 1) -> bytearray | bytes:
-    """
-    Increment sequence id by value
-
-    :return: 4 bytes
-    """
-    if seq_id is None:
-        return b"0000"
-    # Max seq_id = b'FFFB'
-    # b'FFFC' reserved for <unknown> message
-    # b'FFFD' reserved for 'NodeJoinAckResponse' message
-    # b'FFFE' reserved for 'NodeSwitchGroupResponse' message
-    # b'FFFF' reserved for 'NodeAwakeResponse' message
-    if (temp_int := int(seq_id, 16) + value) >= 65532:
-        temp_int = 0
-    temp_str = str(hex(temp_int)).lstrip("0x").upper()
-    while len(temp_str) < 4:
-        temp_str = "0" + temp_str
-    return temp_str.encode()
-
-
-# octals (and hex) type as int according to https://docs.python.org/3/library/stdtypes.html
-def uint_to_int(val: int, octals: int) -> int:
-    """compute the 2's compliment of int value val for negative values"""
-    bits = octals << 2
-    if (val & (1 << (bits - 1))) != 0:
-        val = val - (1 << bits)
-    return val
-
-
-# octals (and hex) type as int according to https://docs.python.org/3/library/stdtypes.html
-def int_to_uint(val: int, octals: int) -> int:
-    """compute the 2's compliment of int value val for negative values"""
-    bits = octals << 2
-    if val < 0:
-        val = val + (1 << bits)
-    return val
-
-
-class BaseType:
-    def __init__(self, value, length) -> None:  # type: ignore[no-untyped-def]
-        self.value = value
-        self.length = length
-
-    def serialize(self):  # type: ignore[no-untyped-def]
-        return bytes(self.value, UTF8_DECODE)
-
-    def deserialize(self, val):  # type: ignore[no-untyped-def]
-        self.value = val
-
-    def __len__(self):  # type: ignore[no-untyped-def]
-        return self.length
-
-
-class CompositeType:
-    def __init__(self) -> None:
-        self.contents: list = []
-        # datetime because of DateTime and Time and RealClockDate
-        self.value: datetime.datetime | datetime.time | datetime.date | None = None
-
-    def serialize(self):  # type: ignore[no-untyped-def]
-        return b"".join(a.serialize() for a in self.contents)
-
-    def deserialize(self, val):  # type: ignore[no-untyped-def]
-        for content in self.contents:
-            myval = val[: len(content)]
-            content.deserialize(myval)
-            val = val[len(myval) :]
-        return val
-
-    def __len__(self):  # type: ignore[no-untyped-def]
-        return sum(len(x) for x in self.contents)
-
-
-class String(BaseType):
-    pass
-
-
-class Int(BaseType):
-    def __init__(self, value, length=2, negative: bool = True) -> None:  # type: ignore[no-untyped-def]
-        super().__init__(value, length)
-        self.negative = negative
-
-    def serialize(self):  # type: ignore[no-untyped-def]
-        fmt = "%%0%dX" % self.length
-        return bytes(fmt % self.value, UTF8_DECODE)
-
-    def deserialize(self, val):  # type: ignore[no-untyped-def]
-        self.value = int(val, 16)
-        if self.negative:
-            mask = 1 << (self.length * 4 - 1)
-            self.value = -(self.value & mask) + (self.value & ~mask)
-
-
-class SInt(BaseType):
-    def __init__(self, value, length=2) -> None:  # type: ignore[no-untyped-def]
-        super().__init__(value, length)
-
-    @staticmethod
-    def negative(val, octals):  # type: ignore[no-untyped-def]
-        """compute the 2's compliment of int value val for negative values"""
-        bits = octals << 2
-        if (val & (1 << (bits - 1))) != 0:
-            val = val - (1 << bits)
-        return val
-
-    def serialize(self):  # type: ignore[no-untyped-def]
-        fmt = "%%0%dX" % self.length
-        return fmt % int_to_uint(self.value, self.length)
-
-    def deserialize(self, val):  # type: ignore[no-untyped-def]
-        # TODO: negative is not initialized! 20220405
-        self.value = self.negative(int(val, 16), self.length)  # type: ignore [no-untyped-call]
-
-
-class UnixTimestamp(Int):
-    def __init__(self, value, length=8) -> None:  # type: ignore[no-untyped-def]
-        Int.__init__(self, value, length, False)
-
-    def deserialize(self, val):  # type: ignore[no-untyped-def]
-        # TODO: Solution, fix Int 20220405
-        Int.deserialize(self, val)  # type: ignore[no-untyped-call]
-        self.value = datetime.datetime.fromtimestamp(self.value)
-
-
-class Year2k(Int):
-    """year value that is offset from the year 2000"""
-
-    def deserialize(self, val):  # type: ignore[no-untyped-def]
-        # TODO: Solution, fix Int 20220405
-        Int.deserialize(self, val)  # type: ignore[no-untyped-call]
-        self.value += PLUGWISE_EPOCH
-
-
-class DateTime(CompositeType):
-    """datetime value as used in the general info response
-    format is: YYMMmmmm
-    where year is offset value from the epoch which is Y2K
-    and last four bytes are offset from the beginning of the month in minutes
-    """
-
-    def __init__(self, year: int = 0, month: int = 1, minutes: int = 0) -> None:
-        CompositeType.__init__(self)
-        self.year = Year2k(year - PLUGWISE_EPOCH, 2)
-        self.month = Int(month, 2, False)
-        self.minutes = Int(minutes, 4, False)
-        self.contents += [self.year, self.month, self.minutes]
-
-    def deserialize(self, val: int) -> None:
-        # TODO: Solution, fix Int 20220405
-        CompositeType.deserialize(self, val)  # type: ignore[no-untyped-call]
-        if self.minutes.value == 65535:
-            self.value = None
-        else:
-            self.value = datetime.datetime(
-                year=self.year.value, month=self.month.value, day=1
-            ) + datetime.timedelta(minutes=self.minutes.value)
-
-
-class Time(CompositeType):
-    """time value as used in the clock info response"""
-
-    def __init__(self, hour: int = 0, minute: int = 0, second: int = 0) -> None:
-        CompositeType.__init__(self)
-        self.hour = Int(hour, 2, False)
-        self.minute = Int(minute, 2, False)
-        self.second = Int(second, 2, False)
-        self.contents += [self.hour, self.minute, self.second]
-
-    def deserialize(self, val) -> None:  # type: ignore[no-untyped-def]
-        # TODO: Solution, fix Int 20220405
-        CompositeType.deserialize(self, val)  # type: ignore[no-untyped-call]
-        self.value = datetime.time(
-            self.hour.value, self.minute.value, self.second.value
-        )
-
-
-class IntDec(BaseType):
-    def __init__(self, value, length=2) -> None:  # type: ignore[no-untyped-def]
-        super().__init__(value, length)
-
-    def serialize(self):  # type: ignore[no-untyped-def]
-        fmt = "%%0%dd" % self.length
-        return bytes(fmt % self.value, UTF8_DECODE)
-
-    def deserialize(self, val):  # type: ignore[no-untyped-def]
-        self.value = val.decode(UTF8_DECODE)
-
-
-class RealClockTime(CompositeType):
-    """time value as used in the realtime clock info response"""
-
-    def __init__(self, hour: int = 0, minute: int = 0, second: int = 0) -> None:
-        CompositeType.__init__(self)
-        self.hour = IntDec(hour, 2)
-        self.minute = IntDec(minute, 2)
-        self.second = IntDec(second, 2)
-        self.contents += [self.second, self.minute, self.hour]
-
-    def deserialize(self, val):  # type: ignore[no-untyped-def]
-        # TODO: Solution, fix Int 20220405
-        CompositeType.deserialize(self, val)  # type: ignore[no-untyped-call]
-        self.value = datetime.time(
-            int(self.hour.value),
-            int(self.minute.value),
-            int(self.second.value),
-        )
-
-
-class RealClockDate(CompositeType):
-    """date value as used in the realtime clock info response"""
-
-    def __init__(self, day: int = 0, month: int = 0, year: int = 0) -> None:
-        CompositeType.__init__(self)
-        self.day = IntDec(day, 2)
-        self.month = IntDec(month, 2)
-        self.year = IntDec(year - PLUGWISE_EPOCH, 2)
-        self.contents += [self.day, self.month, self.year]
-
-    def deserialize(self, val):  # type: ignore[no-untyped-def]
-        # TODO: Solution, fix Int 20220405
-        CompositeType.deserialize(self, val)  # type: ignore[no-untyped-call]
-        self.value = datetime.date(
-            int(self.year.value) + PLUGWISE_EPOCH,
-            int(self.month.value),
-            int(self.day.value),
-        )
-
-
-class Float(BaseType):
-    def __init__(self, value, length=4):  # type: ignore[no-untyped-def]
-        super().__init__(value, length)
-
-    def deserialize(self, val):  # type: ignore[no-untyped-def]
-        hexval = binascii.unhexlify(val)
-        self.value = struct.unpack("!f", hexval)[0]
-
-
-class LogAddr(Int):
-    def serialize(self):  # type: ignore[no-untyped-def]
-        return bytes("%08X" % ((self.value * 32) + LOGADDR_OFFSET), UTF8_DECODE)
-
-    def deserialize(self, val):  # type: ignore[no-untyped-def]
-        # TODO: Solution, fix Int 20220405
-        Int.deserialize(self, val)  # type: ignore[no-untyped-call]
-        self.value = (self.value - LOGADDR_OFFSET) // 32
diff --git a/plugwise/smile/util.py b/plugwise/util.py
similarity index 97%
rename from plugwise/smile/util.py
rename to plugwise/util.py
index 634c545fe..fa5ecab38 100644
--- a/plugwise/smile/util.py
+++ b/plugwise/util.py
@@ -84,8 +84,9 @@ def in_between(
 # NOTE: this function version_to_model is shared between Smile and USB
 def version_to_model(version: str | None) -> str | None:
     """Translate hardware_version to device type."""
-    if version is None:
-        return None
+
+    if version is None:  # pragma: no cover #294
+        return version
 
     model = HW_MODELS.get(version)
     if model is None:
diff --git a/pyproject.toml b/pyproject.toml
index 548780d25..3b0cd7544 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,9 +4,9 @@ build-backend = "setuptools.build_meta"
 
 [project]
 name            = "plugwise"
-version         = "0.30.0a1"
+version         = "0.31.0"
 license         = {file = "LICENSE"}
-description     = "Plugwise Smile (Adam/Anna/P1), Stretch and USB (Stick) module for Python 3."
+description     = "Plugwise Smile (Adam/Anna/P1) and Stretch module for Python 3."
 readme          = "README.md"
 keywords        = ["home", "automation", "plugwise", "module"]
 classifiers     = [
@@ -24,7 +24,6 @@ authors = [
 ]
 maintainers = [
         { name = "bouwew"},
-        { name = "brefra"},
         { name = "CoMPaTech" }
 ]
 requires-python = ">=3.9.0"
@@ -206,16 +205,13 @@ disallow_untyped_defs = true
 no_implicit_optional = true
 warn_return_any = true
 warn_unreachable = true
-exclude = [
-    "plugwise/stick/*",
-]
+exclude = []
 
 [tool.coverage.run]
 source = [ "plugwise" ]
 omit= [
     "*/venv/*",
     "setup.py",
-    "plugwise/stick/*",
 ]
 
 [tool.ruff]
@@ -257,9 +253,7 @@ ignore = [
 #    # Ignored due to performance: https://github.com/charliermarsh/ruff/issues/2923
 #    "UP038", # Use `X | Y` in `isinstance` call instead of `(X, Y)`
 
-exclude = [
-    "plugwise/stick/*",
-]
+exclude = []
 
 [tool.ruff.flake8-import-conventions.extend-aliases]
 voluptuous = "vol"
diff --git a/scripts/complexity.sh b/scripts/complexity.sh
index 902518e73..fbae27090 100755
--- a/scripts/complexity.sh
+++ b/scripts/complexity.sh
@@ -13,7 +13,7 @@ if [ -f "${my_venv}/bin/activate" ]; then
     echo "-----------------------------"
     echo "Running cyclomatic complexity"
     echo "-----------------------------"
-    PYTHONPATH=$(pwd) radon cc plugwise/smile.py plugwise/helper.py tests/test_smile.py -s -nc --no-assert
+    PYTHONPATH=$(pwd) radon cc plugwise/ tests/ -s -nc --no-assert
 else
     echo "Virtualenv available, bailing out"
     exit 2


Test patch:
diff --git a/tests/README.md b/tests/README.md
index 6fde09ed1..786d427f7 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -22,8 +22,6 @@ Data processing (positive)
 
 ## Running tests against Plugwise Smile
 
-`*) This states Smile as unfortunately tests for USB are still to be added`
-
 Our tests are mainly conducted locally by the contributors and using Github Actions, see the workflows in `.github/workflows`. You can always look up the latest (hopefully successful) build at [python-plugwise Github actions](https://github.com/plugwise/python-plugwise/actions).
 
 If you want to run tests locally, use the provided `scripts/setup_test.sh` to initialize and enable your virtualenv using `source venv/bin/activate`.
diff --git a/tests/test_smile.py b/tests/test_smile.py
index 5a88a2382..403e8e236 100644
--- a/tests/test_smile.py
+++ b/tests/test_smile.py
@@ -19,9 +19,9 @@
 from freezegun import freeze_time
 import pytest
 
-pw_constants = importlib.import_module("plugwise.smile.constants")
-pw_exceptions = importlib.import_module("plugwise.smile.exceptions")
-pw_smile = importlib.import_module("plugwise.smile")
+pw_constants = importlib.import_module("plugwise.constants")
+pw_exceptions = importlib.import_module("plugwise.exceptions")
+pw_smile = importlib.import_module("plugwise")
 
 pytestmark = pytest.mark.asyncio
 
@@ -5231,7 +5231,7 @@ async def test_connect_fail_firmware(self):
 
     # Test connect for timeout
     @patch(
-        "plugwise.smile.helper.ClientSession.get",
+        "plugwise.helper.ClientSession.get",
         side_effect=aiohttp.ServerTimeoutError,
     )
     @pytest.mark.asyncio
diff --git a/tests/test_stick.py b/tests/test_stick.py
deleted file mode 100644
index 37eb73ff2..000000000
--- a/tests/test_stick.py
+++ /dev/null
@@ -1,11 +0,0 @@
-# pylint: disable=protected-access
-"""Test Plugwise Stick features."""
-
-import importlib
-
-pw_constants = importlib.import_module("plugwise.stick.constants")
-pw_exceptions = importlib.import_module("plugwise.stick.exceptions")
-pw_stick = importlib.import_module("plugwise.stick")
-
-# No tests available
-assert True