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

Issue:
Fix black and buildqueue-issues
Same as with  split and python-plugwise-usb experience


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml
index e7904bce8..3485eb028 100644
--- a/.github/workflows/verify.yml
+++ b/.github/workflows/verify.yml
@@ -4,7 +4,7 @@
 name: Latest commit
 
 env:
-  CACHE_VERSION: 7
+  CACHE_VERSION: 9
   DEFAULT_PYTHON: "3.11"
   PRE_COMMIT_HOME: ~/.cache/pre-commit
 
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6f68226ea..1ec96febe 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,7 @@
 # Changelog
 
+## Ongoing: Logic separation between USB and non-USB
+
 ## v0.27.10: Anna + Elga: final fix for [#320](https://github.com/plugwise/plugwise-beta/issues/320)
 
 ## v0.27.9: P1 legacy: collect data from /core/modules
diff --git a/README.md b/README.md
index da076721d..0bab31c59 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,8 @@ Our main usage for this module is supporting [Home Assistant](https://www.home-a
 [![Newest commit](https://github.com/plugwise/python-plugwise/workflows/Latest%20commit/badge.svg)](https://github.com/plugwise/python-plugwise/actions)
 [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/plugwise/python-plugwise/main.svg)](https://results.pre-commit.ci/latest/github/plugwise/python-plugwise/main)
 
+The below scoring only applies to Plugwise Smile components:
+
 [![CodeFactor](https://www.codefactor.io/repository/github/plugwise/python-plugwise/badge)](https://www.codefactor.io/repository/github/plugwise/python-plugwise)
 [![codecov](https://codecov.io/gh/plugwise/python-plugwise/branch/main/graph/badge.svg)](https://codecov.io/gh/plugwise/python-plugwise)
 
@@ -46,8 +48,12 @@ If you want to create a PR please make sure you at least run `scripts/setup.sh`.
 
 ## Project support status
 
+**Notice** at this time we are refactoring the module code to move towards a supporting way for the integration to become multiple components under an umbrella `plugwise` integration featuring multiple components.
+
 Module providing interfacing with the Plugwise devices:
 
+### Smile
+
 - [x] Adam
   - [x] Lisa
   - [x] Jip
@@ -58,6 +64,11 @@ Module providing interfacing with the Plugwise devices:
   - [x] Aqara Plug
 - [x] Anna
 - [x] Smile P1
+- [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
@@ -65,7 +76,6 @@ Module providing interfacing with the Plugwise devices:
   The devices listed below have **NOT** been tested and are therefore unknown for their correct operation
     - [x] Sense
     - [x] Switch
-- [x] Stretch
 - [x] [Home-Assistant](https://home-assistant.io) via
   - [x] Native supporting networked Plugwise products
   - [ ] Native supporting USB Plugwise products (in progress)
diff --git a/plugwise/DEPENDENCIES.md b/plugwise/DEPENDENCIES.md
new file mode 100644
index 000000000..557d01a7a
--- /dev/null
+++ b/plugwise/DEPENDENCIES.md
@@ -0,0 +1,16 @@
+# 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/smile.py b/plugwise/smile/__init__.py
similarity index 100%
rename from plugwise/smile.py
rename to plugwise/smile/__init__.py
diff --git a/plugwise/constants.py b/plugwise/smile/constants.py
similarity index 62%
rename from plugwise/constants.py
rename to plugwise/smile/constants.py
index a94ecfff3..8315dff09 100644
--- a/plugwise/constants.py
+++ b/plugwise/smile/constants.py
@@ -1,4 +1,4 @@
-"""Plugwise Stick and Smile constants."""
+"""Plugwise Smile constants."""
 from __future__ import annotations
 
 from collections import namedtuple
@@ -30,341 +30,6 @@
 VOLUME_CUBIC_METERS: Final = "m³"
 VOLUME_CUBIC_METERS_PER_HOUR: Final = "m³/h"
 
-# --- Stick constants ---
-
-UTF8_DECODE: Final = "utf-8"
-
-# 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",
-    "143.1": "ThermoTouch",
-    "159.2": "Adam",
-    "106-03": "Tom/Floor",
-    "158-01": "Lisa",
-    "160-01": "Plug",
-    "168-01": "Jip",
-}
-
-# 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",
-}
-
-
-# --- Smile constants ---
-
 ACTUATOR_CLASSES: Final[tuple[str, ...]] = (
     "heater_central",
     "thermostat",
@@ -406,6 +71,45 @@
     "upper_bound",
     "resolution",
 )
+
+HW_MODELS: Final[dict[str, str]] = {
+    "143.1": "ThermoTouch",
+    "159.2": "Adam",
+    "106-03": "Tom/Floor",
+    "158-01": "Lisa",
+    "160-01": "Plug",
+    "168-01": "Jip",
+    "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",
+}
+
 MAX_SETPOINT: Final[float] = 30.0
 MIN_SETPOINT: Final[float] = 4.0
 SPECIAL_FORMAT: Final[tuple[str, ...]] = (ENERGY_KILO_WATT_HOUR, VOLUME_CUBIC_METERS)
diff --git a/plugwise/exceptions.py b/plugwise/smile/exceptions.py
similarity index 58%
rename from plugwise/exceptions.py
rename to plugwise/smile/exceptions.py
index ccf8ea138..084a55943 100644
--- a/plugwise/exceptions.py
+++ b/plugwise/smile/exceptions.py
@@ -1,52 +1,10 @@
-"""Plugwise Exceptions."""
+"""Plugwise Smile Exceptions."""
 
 
 class PlugwiseException(Exception):
     """Base error class for this Plugwise library."""
 
 
-# --- Stick exceptions ---
-
-
-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."""
-
-
-# --- Smile exceptions ---
-
-
 class ConnectionFailedError(PlugwiseException):
     """Raised when unable to connect."""
 
diff --git a/plugwise/helper.py b/plugwise/smile/helper.py
similarity index 100%
rename from plugwise/helper.py
rename to plugwise/smile/helper.py
diff --git a/plugwise/smile/util.py b/plugwise/smile/util.py
new file mode 100644
index 000000000..634c545fe
--- /dev/null
+++ b/plugwise/smile/util.py
@@ -0,0 +1,97 @@
+"""Plugwise protocol helpers."""
+from __future__ import annotations
+
+import datetime
+import re
+
+from .constants import (
+    ARBITRARY_DATE,
+    ELECTRIC_POTENTIAL_VOLT,
+    ENERGY_KILO_WATT_HOUR,
+    HW_MODELS,
+    PERCENTAGE,
+    SPECIAL_FORMAT,
+    TEMP_CELSIUS,
+)
+
+
+def escape_illegal_xml_characters(xmldata: str) -> str:
+    """Replace illegal &-characters."""
+    return re.sub(r"&([^a-zA-Z#])", r"&amp;\1", xmldata)
+
+
+def format_measure(measure: str, unit: str) -> float | int | bool:
+    """Format measure to correct type."""
+    # TODO: handle with appropriate care 20220405
+    # continuously reassigning the same value with different type isn't
+    # to typings liking
+    result: int | float | bool = False
+    try:
+        result = int(measure)
+        if unit == TEMP_CELSIUS:
+            result = float(measure)
+    except ValueError:
+        try:
+            float_measure = float(measure)
+            if unit == PERCENTAGE:
+                if 0 < float_measure <= 1:
+                    return int(float_measure * 100)
+
+            if unit == ENERGY_KILO_WATT_HOUR:
+                float_measure = float_measure / 1000
+
+            if unit in SPECIAL_FORMAT:
+                result = float(f"{round(float_measure, 3):.3f}")
+            elif unit == ELECTRIC_POTENTIAL_VOLT:
+                result = float(f"{round(float_measure, 1):.1f}")
+            else:
+                if abs(float_measure) < 10:
+                    result = float(f"{round(float_measure, 2):.2f}")
+                elif abs(float_measure) >= 10 and abs(float_measure) < 100:
+                    result = float(f"{round(float_measure, 1):.1f}")
+                elif abs(float_measure) >= 100:
+                    result = int(round(float_measure))
+        except ValueError:
+            if measure in ["on", "true"]:
+                result = True
+            if measure in ["off", "false"]:
+                result = False
+    return result
+
+
+def in_between(
+    today: int,
+    day_0: int,
+    day_1: int,
+    now: datetime.time,
+    time_0: datetime.time,
+    time_1: datetime.time,
+) -> bool:
+    """Determine timing for schedules."""
+    time_now = datetime.timedelta(days=today, hours=now.hour, minutes=now.minute)
+    time_start = datetime.timedelta(
+        days=day_0, hours=time_0.hour, minutes=time_0.minute
+    )
+    time_end = datetime.timedelta(days=day_1, hours=time_1.hour, minutes=time_1.minute)
+
+    now_point = ARBITRARY_DATE + time_now
+    start_point = ARBITRARY_DATE + time_start
+    end_point = ARBITRARY_DATE + time_end
+
+    return start_point <= now_point <= end_point
+
+
+# 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"
diff --git a/plugwise/stick.py b/plugwise/stick/__init__.py
similarity index 99%
rename from plugwise/stick.py
rename to plugwise/stick/__init__.py
index 002dbcabe..bf2e0c08f 100644
--- a/plugwise/stick.py
+++ b/plugwise/stick/__init__.py
@@ -13,6 +13,7 @@
     ACCEPT_JOIN_REQUESTS,
     CB_JOIN_REQUEST,
     CB_NEW_NODE,
+    HW_MODELS,
     MESSAGE_TIME_OUT,
     NODE_TYPE_CELSIUS_NR,
     NODE_TYPE_CELSIUS_SED,
diff --git a/plugwise/connections/__init__.py b/plugwise/stick/connections/__init__.py
similarity index 100%
rename from plugwise/connections/__init__.py
rename to plugwise/stick/connections/__init__.py
diff --git a/plugwise/connections/serial.py b/plugwise/stick/connections/serial.py
similarity index 100%
rename from plugwise/connections/serial.py
rename to plugwise/stick/connections/serial.py
diff --git a/plugwise/connections/socket.py b/plugwise/stick/connections/socket.py
similarity index 100%
rename from plugwise/connections/socket.py
rename to plugwise/stick/connections/socket.py
diff --git a/plugwise/stick/constants.py b/plugwise/stick/constants.py
new file mode 100644
index 000000000..04634a7fc
--- /dev/null
+++ b/plugwise/stick/constants.py
@@ -0,0 +1,355 @@
+"""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/controller.py b/plugwise/stick/controller.py
similarity index 100%
rename from plugwise/controller.py
rename to plugwise/stick/controller.py
diff --git a/plugwise/stick/exceptions.py b/plugwise/stick/exceptions.py
new file mode 100644
index 000000000..c18b1a699
--- /dev/null
+++ b/plugwise/stick/exceptions.py
@@ -0,0 +1,41 @@
+"""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/messages/__init__.py b/plugwise/stick/messages/__init__.py
similarity index 100%
rename from plugwise/messages/__init__.py
rename to plugwise/stick/messages/__init__.py
diff --git a/plugwise/messages/requests.py b/plugwise/stick/messages/requests.py
similarity index 100%
rename from plugwise/messages/requests.py
rename to plugwise/stick/messages/requests.py
diff --git a/plugwise/messages/responses.py b/plugwise/stick/messages/responses.py
similarity index 100%
rename from plugwise/messages/responses.py
rename to plugwise/stick/messages/responses.py
diff --git a/plugwise/nodes/__init__.py b/plugwise/stick/nodes/__init__.py
similarity index 99%
rename from plugwise/nodes/__init__.py
rename to plugwise/stick/nodes/__init__.py
index 2dfeebe07..f5fec0e8e 100644
--- a/plugwise/nodes/__init__.py
+++ b/plugwise/stick/nodes/__init__.py
@@ -2,12 +2,15 @@
 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,
 )
@@ -18,7 +21,6 @@
     NodeJoinAckResponse,
     NodePingResponse,
 )
-from ..util import validate_mac, version_to_model
 
 _LOGGER = logging.getLogger(__name__)
 
diff --git a/plugwise/nodes/circle.py b/plugwise/stick/nodes/circle.py
similarity index 100%
rename from plugwise/nodes/circle.py
rename to plugwise/stick/nodes/circle.py
diff --git a/plugwise/nodes/circle_plus.py b/plugwise/stick/nodes/circle_plus.py
similarity index 100%
rename from plugwise/nodes/circle_plus.py
rename to plugwise/stick/nodes/circle_plus.py
diff --git a/plugwise/nodes/scan.py b/plugwise/stick/nodes/scan.py
similarity index 100%
rename from plugwise/nodes/scan.py
rename to plugwise/stick/nodes/scan.py
diff --git a/plugwise/nodes/sed.py b/plugwise/stick/nodes/sed.py
similarity index 100%
rename from plugwise/nodes/sed.py
rename to plugwise/stick/nodes/sed.py
diff --git a/plugwise/nodes/sense.py b/plugwise/stick/nodes/sense.py
similarity index 100%
rename from plugwise/nodes/sense.py
rename to plugwise/stick/nodes/sense.py
diff --git a/plugwise/nodes/stealth.py b/plugwise/stick/nodes/stealth.py
similarity index 100%
rename from plugwise/nodes/stealth.py
rename to plugwise/stick/nodes/stealth.py
diff --git a/plugwise/nodes/switch.py b/plugwise/stick/nodes/switch.py
similarity index 100%
rename from plugwise/nodes/switch.py
rename to plugwise/stick/nodes/switch.py
diff --git a/plugwise/parser.py b/plugwise/stick/parser.py
similarity index 100%
rename from plugwise/parser.py
rename to plugwise/stick/parser.py
diff --git a/plugwise/util.py b/plugwise/stick/util.py
similarity index 79%
rename from plugwise/util.py
rename to plugwise/stick/util.py
index 455fe0cd8..46c8820e5 100644
--- a/plugwise/util.py
+++ b/plugwise/stick/util.py
@@ -12,32 +12,12 @@
 
 import crcmod
 
-from .constants import (
-    ARBITRARY_DATE,
-    ELECTRIC_POTENTIAL_VOLT,
-    ENERGY_KILO_WATT_HOUR,
-    HW_MODELS,
-    LOGADDR_OFFSET,
-    PERCENTAGE,
-    PLUGWISE_EPOCH,
-    SPECIAL_FORMAT,
-    TEMP_CELSIUS,
-    UTF8_DECODE,
-)
+from .constants import HW_MODELS, LOGADDR_OFFSET, PLUGWISE_EPOCH, UTF8_DECODE
 
 crc_fun = crcmod.mkCrcFun(0x11021, rev=False, initCrc=0x0000, xorOut=0x0000)
 
 
-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
-
-
+# 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:
@@ -53,6 +33,16 @@ def version_to_model(version: str | None) -> str | None:
     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
@@ -92,72 +82,6 @@ def int_to_uint(val: int, octals: int) -> int:
     return val
 
 
-def escape_illegal_xml_characters(xmldata: str) -> str:
-    """Replace illegal &-characters."""
-    return re.sub(r"&([^a-zA-Z#])", r"&amp;\1", xmldata)
-
-
-def format_measure(measure: str, unit: str) -> float | int | bool:
-    """Format measure to correct type."""
-    # TODO: handle with appropriate care 20220405
-    # continuously reassigning the same value with different type isn't
-    # to typings liking
-    result: int | float | bool = False
-    try:
-        result = int(measure)
-        if unit == TEMP_CELSIUS:
-            result = float(measure)
-    except ValueError:
-        try:
-            float_measure = float(measure)
-            if unit == PERCENTAGE:
-                if 0 < float_measure <= 1:
-                    return int(float_measure * 100)
-
-            if unit == ENERGY_KILO_WATT_HOUR:
-                float_measure = float_measure / 1000
-
-            if unit in SPECIAL_FORMAT:
-                result = float(f"{round(float_measure, 3):.3f}")
-            elif unit == ELECTRIC_POTENTIAL_VOLT:
-                result = float(f"{round(float_measure, 1):.1f}")
-            else:
-                if abs(float_measure) < 10:
-                    result = float(f"{round(float_measure, 2):.2f}")
-                elif abs(float_measure) >= 10 and abs(float_measure) < 100:
-                    result = float(f"{round(float_measure, 1):.1f}")
-                elif abs(float_measure) >= 100:
-                    result = int(round(float_measure))
-        except ValueError:
-            if measure in ["on", "true"]:
-                result = True
-            if measure in ["off", "false"]:
-                result = False
-    return result
-
-
-def in_between(
-    today: int,
-    day_0: int,
-    day_1: int,
-    now: datetime.time,
-    time_0: datetime.time,
-    time_1: datetime.time,
-) -> bool:
-    """Determine timing for schedules."""
-    time_now = datetime.timedelta(days=today, hours=now.hour, minutes=now.minute)
-    time_start = datetime.timedelta(
-        days=day_0, hours=time_0.hour, minutes=time_0.minute
-    )
-    time_end = datetime.timedelta(days=day_1, hours=time_1.hour, minutes=time_1.minute)
-
-    now_point = ARBITRARY_DATE + time_now
-    start_point = ARBITRARY_DATE + time_start
-    end_point = ARBITRARY_DATE + time_end
-
-    return start_point <= now_point <= end_point
-
-
 class BaseType:
     def __init__(self, value, length) -> None:  # type: ignore[no-untyped-def]
         self.value = value
diff --git a/pyproject.toml b/pyproject.toml
index 0dcc70d05..548780d25 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
 
 [project]
 name            = "plugwise"
-version         = "0.27.10"
+version         = "0.30.0a1"
 license         = {file = "LICENSE"}
 description     = "Plugwise Smile (Adam/Anna/P1), Stretch and USB (Stick) module for Python 3."
 readme          = "README.md"
@@ -207,12 +207,7 @@ no_implicit_optional = true
 warn_return_any = true
 warn_unreachable = true
 exclude = [
-    "controller.py",
-    "stick.py",
-    "parser.py",
-    "nodes",
-    "messages",
-    "connections",
+    "plugwise/stick/*",
 ]
 
 [tool.coverage.run]
@@ -220,13 +215,7 @@ source = [ "plugwise" ]
 omit= [
     "*/venv/*",
     "setup.py",
-    "plugwise/connections/*",
-    "plugwise/controller.py",
-    "plugwise/messages/*",
-    "plugwise/nodes/*",
-    "plugwise/parser.py",
-    "plugwise/stick.py",
-    "plugwise/util.py",
+    "plugwise/stick/*",
 ]
 
 [tool.ruff]
@@ -269,13 +258,7 @@ ignore = [
 #    "UP038", # Use `X | Y` in `isinstance` call instead of `(X, Y)`
 
 exclude = [
-    "plugwise/connections/*.py",
-    "plugwise/messages/*.py",
-    "plugwise/nodes/*.py",
-    "plugwise/controller.py",
-    "plugwise/parser.py",
-    "plugwise/stick.py",
-    "plugwise/util.py"
+    "plugwise/stick/*",
 ]
 
 [tool.ruff.flake8-import-conventions.extend-aliases]


Test patch:
diff --git a/scripts/tests_and_coverage.sh b/scripts/tests_and_coverage.sh
index 61411494c..5cd60225d 100755
--- a/scripts/tests_and_coverage.sh
+++ b/scripts/tests_and_coverage.sh
@@ -23,7 +23,7 @@ set +u
 
 if [ -z "${GITHUB_ACTIONS}" ] || [ "$1" == "test_and_coverage" ] ; then
     # Python tests (rerun with debug if failures)
-    PYTHONPATH=$(pwd) pytest -qx tests/test_smile.py --cov='.' --no-cov-on-fail --cov-report term-missing || PYTHONPATH=$(pwd) pytest -xrpP --log-level debug tests/test_smile.py 
+    PYTHONPATH=$(pwd) pytest -qx tests/ --cov='.' --no-cov-on-fail --cov-report term-missing || PYTHONPATH=$(pwd) pytest -xrpP --log-level debug tests/
 fi
 
 if [ -z "${GITHUB_ACTIONS}" ] || [ "$1" == "linting" ] ; then
diff --git a/tests/test_smile.py b/tests/test_smile.py
index 462c897be..5a88a2382 100644
--- a/tests/test_smile.py
+++ b/tests/test_smile.py
@@ -19,9 +19,9 @@
 from freezegun import freeze_time
 import pytest
 
-pw_exceptions = importlib.import_module("plugwise.exceptions")
+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")
 
 pytestmark = pytest.mark.asyncio
 
@@ -5230,7 +5230,10 @@ async def test_connect_fail_firmware(self):
             assert True
 
     # Test connect for timeout
-    @patch("plugwise.helper.ClientSession.get", side_effect=aiohttp.ServerTimeoutError)
+    @patch(
+        "plugwise.smile.helper.ClientSession.get",
+        side_effect=aiohttp.ServerTimeoutError,
+    )
     @pytest.mark.asyncio
     async def test_connect_timeout(self, timeout_test):
         """Wrap connect to raise timeout during get."""
diff --git a/tests/test_stick.py b/tests/test_stick.py
new file mode 100644
index 000000000..37eb73ff2
--- /dev/null
+++ b/tests/test_stick.py
@@ -0,0 +1,11 @@
+# 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