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

Issue:
Small testing improvements
Small improvements ... wondering if the `with async_io timeout` in `request` ever would reach it's exception, needs more inspection. (It seems that the intended timeouts from the testing class only get `ResponseError` back and no raise of `aiohttp.timeouterror`. That (now missing coverage) exception should be tested. Also the websession structure needs improvement in module and accompanying tests later on.

Fixed some small pylint things, bluntly test missing websession and added the `delete_notification` test.
Small testing improvements
Small improvements ... wondering if the `with async_io timeout` in `request` ever would reach it's exception, needs more inspection. (It seems that the intended timeouts from the testing class only get `ResponseError` back and no raise of `aiohttp.timeouterror`. That (now missing coverage) exception should be tested. Also the websession structure needs improvement in module and accompanying tests later on.

Fixed some small pylint things, bluntly test missing websession and added the `delete_notification` test.


Discussion / hints:

Fix patch:
diff --git a/plugwise/__init__.py b/plugwise/__init__.py
index 1d3aa43b5..ab274cebb 100644
--- a/plugwise/__init__.py
+++ b/plugwise/__init__.py
@@ -1,6 +1,6 @@
 """Plugwise module."""
 
-__version__ = "0.9.1"
+__version__ = "0.9.2"
 
 from plugwise.smile import Smile
 from plugwise.stick import stick
diff --git a/plugwise/constants.py b/plugwise/constants.py
index 8f2184b86..948daa113 100644
--- a/plugwise/constants.py
+++ b/plugwise/constants.py
@@ -195,6 +195,10 @@
     "120006": "Sense Legrand",
     "070051": "Switch",
     "080029": "Switch",
+    "160-01": "Plug",
+    "106-03": "Tom/Floor",
+    "158-01": "Lisa",
+    "143.1": "Anna",
 }
 
 # Defaults for SED's (Sleeping End Devices)
@@ -348,7 +352,11 @@
 DEFAULT_PORT = 80
 
 SWITCH_GROUP_TYPES = ["switching", "report"]
-
+THERMOSTAT_CLASSES = [
+    "thermostat",
+    "zone_thermostat",
+    "thermostatic_radiator_valve",
+]
 HOME_MEASUREMENTS = {
     "electricity_consumed": {
         ATTR_TYPE: "power",
@@ -378,7 +386,23 @@
     "temperature": {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS},
     # HA Core setpoint
     "thermostat": {ATTR_NAME: "setpoint", ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS},
-    # Anna/Adam
+    "outdoor_temperature": {
+        ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS
+    },  # Outdoor temp as reported on the Anna, in the App
+    "schedule_temperature": {
+        ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS
+    },  # Only present on legacy Anna and Anna_v3
+    # Lisa and Tom
+    "battery": {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE},
+    "temperature_difference": {ATTR_UNIT_OF_MEASUREMENT: DEGREE},
+    "valve_position": {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE},
+    # Plug
+    "electricity_consumed": {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT},
+    "electricity_produced": {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT},
+    "relay": {ATTR_UNIT_OF_MEASUREMENT: None},
+}
+
+HEATER_CENTRAL_MEASUREMENTS = {
     "boiler_temperature": {
         ATTR_NAME: "water_temperature",
         ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS,
@@ -416,24 +440,10 @@
         ATTR_NAME: "water_pressure",
         ATTR_UNIT_OF_MEASUREMENT: PRESSURE_BAR,
     },
-    "outdoor_temperature": {
-        ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS
-    },  # Outdoor temp as reported on the Anna, in the App
-    "schedule_temperature": {
-        ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS
-    },  # Only present on legacy Anna and Anna_v3
     # Legacy Anna: similar to flame-state on Anna/Adam
     "boiler_state": {ATTR_UNIT_OF_MEASUREMENT: None},
     # Legacy Anna: shows when heating is active, don't show dhw_state, cannot be determined reliably
     "intended_boiler_state": {ATTR_UNIT_OF_MEASUREMENT: None},
-    # Lisa and Tom
-    "battery": {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE},
-    "temperature_difference": {ATTR_UNIT_OF_MEASUREMENT: DEGREE},
-    "valve_position": {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE},
-    # Plug
-    "electricity_consumed": {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT},
-    "electricity_produced": {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT},
-    "relay": {ATTR_UNIT_OF_MEASUREMENT: None},
 }
 
 SMILES = {
diff --git a/plugwise/helper.py b/plugwise/helper.py
new file mode 100644
index 000000000..ff579dfb3
--- /dev/null
+++ b/plugwise/helper.py
@@ -0,0 +1,918 @@
+"""
+Use of this source code is governed by the MIT license found in the LICENSE file.
+
+Plugwise protocol helpers
+"""
+import asyncio
+import datetime as dt
+import logging
+
+import async_timeout
+from dateutil.parser import parse
+from defusedxml import ElementTree as etree
+
+# Time related
+import pytz
+
+from .constants import (
+    APPLIANCES,
+    ATTR_NAME,
+    ATTR_TYPE,
+    ATTR_UNIT_OF_MEASUREMENT,
+    DEVICE_MEASUREMENTS,
+    DOMAIN_OBJECTS,
+    ENERGY_KILO_WATT_HOUR,
+    ENERGY_WATT_HOUR,
+    HEATER_CENTRAL_MEASUREMENTS,
+    HOME_MEASUREMENTS,
+    LOCATIONS,
+    POWER_WATT,
+    SWITCH_GROUP_TYPES,
+)
+from .exceptions import (
+    DeviceTimeoutError,
+    InvalidAuthentication,
+    InvalidXMLError,
+    ResponseError,
+)
+from .util import (
+    determine_selected,
+    escape_illegal_xml_characters,
+    format_measure,
+    in_between,
+    version_to_model,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def check_model(name, v_name):
+    """Model checking before using version_to_model."""
+    if v_name in ["Plugwise", "Plugwise B.V."]:
+        if name == "ThermoTouch":
+            return "Anna"
+        model = version_to_model(name)
+        if model != "Unknown":
+            return model
+    else:
+        return name
+
+
+def types_finder(data):
+    """Detect types within locations from logs."""
+    types = set()
+    for measure, attrs in HOME_MEASUREMENTS.items():
+        locator = f".//logs/point_log[type='{measure}']"
+        if data.find(locator) is not None:
+            log = data.find(locator)
+
+            if measure == "outdoor_temperature":
+                types.add(attrs[ATTR_TYPE])
+
+            p_locator = ".//electricity_point_meter"
+            if log.find(p_locator) is not None:
+                if log.find(p_locator).get("id"):
+                    types.add(attrs[ATTR_TYPE])
+
+    return types
+
+
+class SmileHelper:
+    """Define the SmileHelper object."""
+
+    def __init__(self):
+        """Set the constructor for this class."""
+        self._auth = None
+        self._cp_state = None
+        self._endpoint = None
+        self._home_location = None
+        self._smile_legacy = False
+        self._host = None
+        self._loc_data = {}
+        self._port = None
+        self._timeout = None
+
+        self._appliances = None
+        self._domain_objects = None
+        self._locations = None
+        self._modules = None
+
+        self.active_device_present = False
+        self.appl_data = {}
+        self.gateway_id = None
+        self.heater_id = None
+        self.notifications = {}
+        self.smile_hostname = None
+        self.smile_name = None
+        self.smile_type = None
+        self.smile_version = ()
+        self.thermo_locs = None
+        self.websession = None
+
+    async def request(
+        self,
+        command,
+        retry=3,
+        method="get",
+        data=None,
+        headers=None,
+    ):
+        """Request data."""
+        resp = None
+        url = f"{self._endpoint}{command}"
+
+        try:
+            with async_timeout.timeout(self._timeout):
+                if method == "get":
+                    # Work-around for Stretchv2, should not hurt the other smiles
+                    headers = {"Accept-Encoding": "gzip"}
+                    resp = await self.websession.get(
+                        url, auth=self._auth, headers=headers
+                    )
+                if method == "put":
+                    headers = {"Content-type": "text/xml"}
+                    resp = await self.websession.put(
+                        url, data=data, headers=headers, auth=self._auth
+                    )
+                if method == "delete":
+                    resp = await self.websession.delete(url, auth=self._auth)
+        except asyncio.TimeoutError:
+            if retry < 1:
+                _LOGGER.error("Timed out sending command to Plugwise: %s", command)
+                raise DeviceTimeoutError
+            return await self.request(command, retry - 1)
+
+        # Command accepted gives empty body with status 202
+        if resp.status == 202:
+            return
+        # Cornercase for stretch not responding with 202
+        if method == "put" and resp.status == 200:
+            return
+
+        if resp.status == 401:
+            raise InvalidAuthentication
+
+        result = await resp.text()
+        if not result or "<error>" in result:
+            _LOGGER.error("Smile response empty or error in %s", result)
+            raise ResponseError
+
+        try:
+            # Encode to ensure utf8 parsing
+            xml = etree.XML(escape_illegal_xml_characters(result).encode())
+        except etree.ParseError:
+            _LOGGER.error("Smile returns invalid XML for %s", self._endpoint)
+            raise InvalidXMLError
+
+        return xml
+
+    def all_locations(self):
+        """Determine available locations from inventory."""
+        self._loc_data = {}
+
+        # Legacy Anna without outdoor_temp and Stretches have no locations, create one containing all appliances
+        if len(self._locations) == 0 and self._smile_legacy:
+            appliances = set()
+            home_location = 0
+
+            # Add Anna appliances
+            for appliance in self._appliances:
+                appliances.add(appliance.attrib["id"])
+
+            if self.smile_type == "thermostat":
+                self._loc_data[0] = {
+                    "name": "Legacy Anna",
+                    "types": {"temperature"},
+                    "members": appliances,
+                }
+            if self.smile_type == "stretch":
+                self._loc_data[0] = {
+                    "name": "Legacy Stretch",
+                    "types": {"power"},
+                    "members": appliances,
+                }
+
+            self._home_location = home_location
+
+            return
+
+        for location in self._locations:
+            location_name = location.find("name").text
+            location_id = location.attrib["id"]
+            location_types = set()
+            location_members = set()
+
+            # Group of appliances
+            locator = ".//appliances/appliance"
+            if location.find(locator) is not None:
+                for member in location.findall(locator):
+                    location_members.add(member.attrib["id"])
+
+            if location_name == "Home":
+                home_location = location_id
+                location_types.add("home")
+
+                for location_type in types_finder(location):
+                    location_types.add(location_type)
+
+            # Legacy P1 right location has 'services' filled
+            # test data has 5 for example
+            locator = ".//services"
+            if (
+                self._smile_legacy
+                and self.smile_type == "power"
+                and len(location.find(locator)) > 0
+            ):
+                # Override location name found to match
+                location_name = "Home"
+                home_location = location_id
+                location_types.add("home")
+                location_types.add("power")
+
+            self._loc_data[location_id] = {
+                "name": location_name,
+                "types": location_types,
+                "members": location_members,
+            }
+
+        self._home_location = home_location
+
+        return
+
+    def all_appliances(self):
+        """Determine available appliances from inventory."""
+        self.appl_data = {}
+        stretch_v2 = self.smile_type == "stretch" and self.smile_version[1].major == 2
+        stretch_v3 = self.smile_type == "stretch" and self.smile_version[1].major == 3
+
+        self.all_locations()
+
+        if self._smile_legacy and self.smile_type == "power":
+            # Inject home_location as dev_id for legacy so
+            # _appliance_data can use loc_id for dev_id.
+            self.appl_data[self._home_location] = {
+                "name": "P1",
+                "model": "Smile P1",
+                "types": {"power", "home"},
+                "class": "gateway",
+                "location": self._home_location,
+            }
+            self.gateway_id = self._home_location
+
+            return
+
+        # TODO: add locations with members as appliance as well
+        # example 'electricity consumed/produced and relay' on Adam
+        # Basically walk locations for 'members' not set[] and
+        # scan for the same functionality
+
+        # The presence of either indicates a local active device, e.g. heat-pump or gas-fired heater
+        self._cp_state = self._appliances.find(
+            ".//logs/point_log[type='compressor_state']"
+        )
+        fl_state = self._appliances.find(".//logs/point_log[type='flame_state']")
+        bl_state = self._appliances.find(".//services/boiler_state")
+        if self._cp_state or fl_state or bl_state:
+            self.active_device_present = True
+
+        for appliance in self._appliances:
+            appliance_location = None
+            appliance_types = set()
+
+            appliance_id = appliance.attrib["id"]
+            appliance_class = appliance.find("type").text
+            appliance_name = appliance.find("name").text
+            appliance_model = appliance_class.replace("_", " ").title()
+            appliance_fw = None
+            appliance_v_name = None
+
+            # Nothing useful in opentherm so skip it
+            if appliance_class == "open_therm_gateway":
+                continue
+
+            # Find gateway and heater_central devices
+            if appliance_class == "gateway":
+                self.gateway_id = appliance.attrib["id"]
+                appliance_fw = self.smile_version[0]
+                appliance_model = appliance_name = self.smile_name
+                appliance_v_name = "Plugwise B.V."
+
+            if appliance_class in [
+                "thermostat",
+                "thermostatic_radiator_valve",
+                "zone_thermostat",
+            ]:
+                locator = ".//logs/point_log[type='thermostat']/thermostat"
+                mod_type = "thermostat"
+                module_data = self.get_module_data(appliance, locator, mod_type)
+                appliance_v_name = module_data[0]
+                appliance_model = check_model(module_data[1], appliance_v_name)
+                appliance_fw = module_data[3]
+
+            if appliance_class == "heater_central":
+                # Remove heater_central when no active device present
+                if not self.active_device_present:
+                    continue
+
+                self.heater_id = appliance.attrib["id"]
+                appliance_name = "Auxiliary"
+                locator1 = ".//logs/point_log[type='flame_state']/boiler_state"
+                locator2 = ".//services/boiler_state"
+                mod_type = "boiler_state"
+                module_data = self.get_module_data(appliance, locator1, mod_type)
+                if module_data == [None, None, None, None]:
+                    module_data = self.get_module_data(appliance, locator2, mod_type)
+                appliance_v_name = module_data[0]
+                appliance_model = check_model(module_data[1], appliance_v_name)
+                if appliance_model is None:
+                    appliance_model = (
+                        "Generic heater/cooler" if self._cp_state else "Generic heater"
+                    )
+
+            if stretch_v2 or stretch_v3:
+                locator = ".//services/electricity_point_meter"
+                mod_type = "electricity_point_meter"
+                module_data = self.get_module_data(appliance, locator, mod_type)
+                appliance_v_name = module_data[0]
+                if appliance_model != "Group Switch":
+                    appliance_model = None
+                if module_data[2] is not None:
+                    hw_version = module_data[2].replace("-", "")
+                    appliance_model = version_to_model(hw_version)
+                appliance_fw = module_data[3]
+
+            # Appliance with location (i.e. a device)
+            if appliance.find("location") is not None:
+                appliance_location = appliance.find("location").attrib["id"]
+                for appl_type in types_finder(appliance):
+                    appliance_types.add(appl_type)
+            else:
+                # Return all types applicable to home
+                appliance_types = self._loc_data[self._home_location]["types"]
+
+            # Determine appliance_type from functionality
+            if (
+                appliance.find(".//actuator_functionalities/relay_functionality")
+                is not None
+                or appliance.find(".//actuators/relay") is not None
+            ):
+                appliance_types.add("plug")
+            elif (
+                appliance.find(".//actuator_functionalities/thermostat_functionality")
+                is not None
+            ):
+                appliance_types.add("thermostat")
+
+            if self.smile_type != "stretch" and "plug" in appliance_types:
+                locator = ".//logs/point_log/electricity_point_meter"
+                mod_type = "electricity_point_meter"
+                module_data = self.get_module_data(appliance, locator, mod_type)
+                appliance_v_name = module_data[0]
+                appliance_model = version_to_model(module_data[1])
+                appliance_fw = module_data[3]
+
+            self.appl_data[appliance_id] = {
+                "class": appliance_class,
+                "fw": appliance_fw,
+                "location": appliance_location,
+                "model": appliance_model,
+                "name": appliance_name,
+                "types": appliance_types,
+                "vendor": appliance_v_name,
+            }
+
+        # For legacy Anna gateway and heater_central is the same device
+        if self._smile_legacy and self.smile_type == "thermostat":
+            self.gateway_id = self.heater_id
+
+        return
+
+    def get_module_data(self, appliance, locator, mod_type):
+        """Helper functie for finding info in MODULES."""
+        appl_search = appliance.find(locator)
+        if appl_search is not None:
+            link_id = appl_search.attrib["id"]
+            module = self._modules.find(f".//{mod_type}[@id='{link_id}']....")
+            if module is not None:
+                v_name = module.find("vendor_name").text
+                v_model = module.find("vendor_model").text
+                hw_version = module.find("hardware_version").text
+                fw_version = module.find("firmware_version").text
+
+                return [v_name, v_model, hw_version, fw_version]
+        return [None, None, None, None]
+
+    def match_locations(self):
+        """Update locations with used types of appliances."""
+        matched_locations = {}
+
+        self.all_appliances()
+        for location_id, location_details in self._loc_data.items():
+            for dummy, appliance_details in self.appl_data.items():
+                if appliance_details["location"] == location_id:
+                    for appl_type in appliance_details["types"]:
+                        location_details["types"].add(appl_type)
+
+            matched_locations[location_id] = location_details
+
+        return matched_locations
+
+    def presets(self, loc_id):
+        """Get the presets from the thermostat based on location_id."""
+        presets = {}
+        tag = "zone_setpoint_and_state_based_on_preset"
+
+        if self._smile_legacy:
+            return self.presets_legacy()
+
+        rule_ids = self.rule_ids_by_tag(tag, loc_id)
+        if rule_ids is None:
+            rule_ids = self.rule_ids_by_name("Thermostat presets", loc_id)
+            if rule_ids is None:
+                return presets
+
+        for rule_id in rule_ids:
+            directives = self._domain_objects.find(f'rule[@id="{rule_id}"]/directives')
+
+            for directive in directives:
+                preset = directive.find("then").attrib
+                keys, dummy = zip(*preset.items())
+                if str(keys[0]) == "setpoint":
+                    presets[directive.attrib["preset"]] = [float(preset["setpoint"]), 0]
+                else:
+                    presets[directive.attrib["preset"]] = [
+                        float(preset["heating_setpoint"]),
+                        float(preset["cooling_setpoint"]),
+                    ]
+
+        return presets
+
+    # LEGACY Anna function
+    def presets_legacy(self):
+        """Get presets from domain_objects for legacy Smile."""
+        preset_dictionary = {}
+        for directive in self._domain_objects.findall("rule/directives/when/then"):
+            if directive is not None and "icon" in directive.keys():
+                # Ensure list of heating_setpoint, cooling_setpoint
+                preset_dictionary[directive.attrib["icon"]] = [
+                    float(directive.attrib["temperature"]),
+                    0,
+                ]
+
+        return preset_dictionary
+
+    def rule_ids_by_name(self, name, loc_id):
+        """Obtain the rule_id on the given name and location_id."""
+        schema_ids = {}
+        locator = f'.//contexts/context/zone/location[@id="{loc_id}"]'
+        for rule in self._domain_objects.findall(f'.//rule[name="{name}"]'):
+            if rule.find(locator) is not None:
+                schema_ids[rule.attrib["id"]] = loc_id
+
+        if schema_ids != {}:
+            return schema_ids
+
+    def rule_ids_by_tag(self, tag, loc_id):
+        """Obtain the rule_id based on the given template_tag and location_id."""
+        schema_ids = {}
+        locator1 = f'.//template[@tag="{tag}"]'
+        locator2 = f'.//contexts/context/zone/location[@id="{loc_id}"]'
+        for rule in self._domain_objects.findall(".//rule"):
+            if rule.find(locator1) is not None:
+                if rule.find(locator2) is not None:
+                    schema_ids[rule.attrib["id"]] = loc_id
+
+        if schema_ids != {}:
+            return schema_ids
+
+    def temperature_uri_legacy(self):
+        """Determine the location-set_temperature uri - from APPLIANCES."""
+        locator = ".//appliance[type='thermostat']"
+        appliance_id = self._appliances.find(locator).attrib["id"]
+
+        return f"{APPLIANCES};id={appliance_id}/thermostat"
+
+    async def update_domain_objects(self):
+        """Request domain_objects data."""
+        self._domain_objects = await self.request(DOMAIN_OBJECTS)
+
+        # If Plugwise notifications present:
+        self.notifications = {}
+        url = f"{self._endpoint}{DOMAIN_OBJECTS}"
+        notifications = self._domain_objects.findall(".//notification")
+        for notification in notifications:
+            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:
+                _LOGGER.info(
+                    "Plugwise notification present but unable to process, manually investigate: %s",
+                    url,
+                )
+
+    def appliance_data(self, dev_id):
+        """
+        Obtain the appliance-data connected to a location.
+        Determined from APPLIANCES or legacy DOMAIN_OBJECTS.
+        """
+        data = {}
+        search = self._appliances
+
+        if self._smile_legacy and self.smile_type != "stretch":
+            search = self._domain_objects
+
+        appliances = search.findall(f'.//appliance[@id="{dev_id}"]')
+
+        for appliance in appliances:
+            measurements = DEVICE_MEASUREMENTS.items()
+            if self.active_device_present:
+                measurements = {
+                    **DEVICE_MEASUREMENTS,
+                    **HEATER_CENTRAL_MEASUREMENTS,
+                }.items()
+            for measurement, attrs in measurements:
+
+                p_locator = (
+                    f'.//logs/point_log[type="{measurement}"]/period/measurement'
+                )
+                if appliance.find(p_locator) is not None:
+                    if self._smile_legacy:
+                        if measurement == "domestic_hot_water_state":
+                            continue
+
+                    measure = appliance.find(p_locator).text
+                    # Fix for Adam + Anna: there is a pressure-measurement with an unrealistic value,
+                    # this measurement appears at power-on and is never updated, therefore remove.
+                    if (
+                        measurement == "central_heater_water_pressure"
+                        and float(measure) > 3.5
+                    ):
+                        continue
+
+                    try:
+                        measurement = attrs[ATTR_NAME]
+                    except KeyError:
+                        pass
+
+                    data[measurement] = format_measure(
+                        measure, attrs[ATTR_UNIT_OF_MEASUREMENT]
+                    )
+
+                i_locator = (
+                    f'.//logs/interval_log[type="{measurement}"]/period/measurement'
+                )
+                if appliance.find(i_locator) is not None:
+                    name = f"{measurement}_interval"
+                    measure = appliance.find(i_locator).text
+
+                    data[name] = format_measure(measure, ENERGY_WATT_HOUR)
+
+        # Fix for Adam + Anna: heating_state also present under Anna, remove
+        if "temperature" in data:
+            data.pop("heating_state", None)
+
+        return data
+
+    def scan_thermostats(self, debug_text="missing text"):
+        """Update locations with actual master/slave thermostats."""
+        self.thermo_locs = self.match_locations()
+
+        thermo_matching = {
+            "thermostat": 3,
+            "zone_thermostat": 2,
+            "thermostatic_radiator_valve": 1,
+        }
+
+        high_prio = 0
+        for loc_id, location_details in self.thermo_locs.items():
+            self.thermo_locs[loc_id] = location_details
+
+            if loc_id != self._home_location:
+                self.thermo_locs[loc_id].update(
+                    {"master": None, "master_prio": 0, "slaves": set()}
+                )
+            elif self._smile_legacy:
+                self.thermo_locs[loc_id].update(
+                    {"master": None, "master_prio": 0, "slaves": set()}
+                )
+
+            for appliance_id, appliance_details in self.appl_data.items():
+
+                appl_class = appliance_details["class"]
+                if (
+                    loc_id == appliance_details["location"]
+                    or (self._smile_legacy and not appliance_details["location"])
+                ) and appl_class in thermo_matching:
+
+                    # Pre-elect new master
+                    if (
+                        thermo_matching[appl_class]
+                        > self.thermo_locs[loc_id]["master_prio"]
+                    ):
+
+                        # Demote former master
+                        if self.thermo_locs[loc_id]["master"] is not None:
+                            self.thermo_locs[loc_id]["slaves"].add(
+                                self.thermo_locs[loc_id]["master"]
+                            )
+
+                        # Crown master
+                        self.thermo_locs[loc_id]["master_prio"] = thermo_matching[
+                            appl_class
+                        ]
+                        self.thermo_locs[loc_id]["master"] = appliance_id
+
+                    else:
+                        self.thermo_locs[loc_id]["slaves"].add(appliance_id)
+
+                # Find highest ranking thermostat
+                if appl_class in thermo_matching:
+                    if thermo_matching[appl_class] > high_prio:
+                        high_prio = thermo_matching[appl_class]
+
+    def temperature_uri(self, loc_id):
+        """Determine the location-set_temperature uri - from LOCATIONS."""
+        if self._smile_legacy:
+            return self.temperature_uri_legacy()
+
+        locator = f'location[@id="{loc_id}"]/actuator_functionalities/thermostat_functionality'
+        thermostat_functionality_id = self._locations.find(locator).attrib["id"]
+
+        return f"{LOCATIONS};id={loc_id}/thermostat;id={thermostat_functionality_id}"
+
+    def group_switches(self):
+        """Provide switching- or pump-groups, from DOMAIN_OBJECTS."""
+        switch_groups = {}
+        search = self._domain_objects
+
+        appliances = search.findall("./appliance")
+        groups = search.findall("./group")
+
+        for group in groups:
+            group_appl = {}
+            members = []
+            group_id = group.attrib["id"]
+            group_name = group.find("name").text
+            group_type = group.find("type").text
+            if self.smile_type == "stretch":
+                group_appliance = group.findall("appliances/appliance")
+                for dummy in group_appliance:
+                    members.append(dummy.attrib["id"])
+            else:
+                for appliance in appliances:
+                    if appliance.find("./groups/group") is not None:
+                        appl_id = appliance.attrib["id"]
+                        apl_gr_id = appliance.find("./groups/group").attrib["id"]
+                        if apl_gr_id == group_id:
+                            members.append(appl_id)
+
+            if group_type in SWITCH_GROUP_TYPES:
+                group_appl[group_id] = {
+                    "class": group_type,
+                    "fw": None,
+                    "location": None,
+                    "members": members,
+                    "model": "Group Switch",
+                    "name": group_name,
+                    "types": {"switch_group"},
+                    "vendor": "Plugwise",
+                }
+
+            switch_groups.update(group_appl)
+
+        return switch_groups
+
+    def heating_valves(self):
+        """Obtain the amount of open valves used for direct heating, from APPLIANCES."""
+        loc_found = 0
+        open_valve_count = 0
+        for appliance in self._appliances.findall(".//appliance"):
+            locator = './/logs/point_log[type="valve_position"]/period/measurement'
+            if appliance.find(locator) is not None:
+                loc_found += 1
+                measure = appliance.find(locator).text
+                if float(measure) > 0.0:
+                    open_valve_count += 1
+
+        return None if loc_found == 0 else open_valve_count
+
+    def power_data_from_location(self, loc_id):
+        """Obtain the power-data from domain_objects based on location."""
+        direct_data = {}
+        search = self._domain_objects
+        t_string = "tariff"
+        if self._smile_legacy and self.smile_type == "power":
+            t_string = "tariff_indicator"
+
+        loc_logs = search.find(f'.//location[@id="{loc_id}"]/logs')
+
+        if loc_logs is None:
+            return
+
+        log_list = ["point_log", "cumulative_log", "interval_log"]
+        peak_list = ["nl_peak", "nl_offpeak"]
+
+        # meter_string = ".//{}[type='{}']/"
+        for measurement, attrs in HOME_MEASUREMENTS.items():
+            for log_type in log_list:
+                for peak_select in peak_list:
+                    locator = (
+                        f'.//{log_type}[type="{measurement}"]/period/'
+                        f'measurement[@{t_string}="{peak_select}"]'
+                    )
+                    # Only once try to find P1 Legacy values
+                    if loc_logs.find(locator) is None and self.smile_type == "power":
+                        locator = (
+                            f'.//{log_type}[type="{measurement}"]/period/measurement'
+                        )
+
+                        # Skip peak if not split (P1 Legacy)
+                        if peak_select == "nl_offpeak":
+                            continue
+
+                    if loc_logs.find(locator) is None:
+                        continue
+
+                    peak = peak_select.split("_")[1]
+                    if peak == "offpeak":
+                        peak = "off_peak"
+                    log_found = log_type.split("_")[0]
+                    key_string = f"{measurement}_{peak}_{log_found}"
+                    net_string = f"net_electricity_{log_found}"
+                    val = loc_logs.find(locator).text
+                    f_val = format_measure(val, attrs[ATTR_UNIT_OF_MEASUREMENT])
+                    # Format only HOME_MEASUREMENT POWER_WATT values, do not move to util-format_meaure function!
+                    if attrs[ATTR_UNIT_OF_MEASUREMENT] == POWER_WATT:
+                        f_val = int(round(float(val)))
+                    if all(
+                        item in key_string for item in ["electricity", "cumulative"]
+                    ):
+                        f_val = format_measure(val, ENERGY_KILO_WATT_HOUR)
+                    # Energy differential
+                    if "electricity" in measurement:
+                        diff = 1
+                        if "produced" in measurement:
+                            diff = -1
+                        if net_string not in direct_data:
+                            direct_data[net_string] = 0
+                        if isinstance(f_val, int):
+                            direct_data[net_string] += f_val * diff
+                        else:
+                            direct_data[net_string] += float(f_val * diff)
+
+                    if "gas" in measurement:
+                        key_string = f"{measurement}_{log_found}"
+
+                    direct_data[key_string] = f_val
+
+        if direct_data != {}:
+            return direct_data
+
+    def preset(self, loc_id):
+        """
+        Obtain the active preset based on the location_id.
+        Determined from DOMAIN_OBJECTS.
+        """
+        if self._smile_legacy:
+            active_rule = self._domain_objects.find(
+                "rule[active='true']/directives/when/then"
+            )
+            if active_rule is None or "icon" not in active_rule.keys():
+                return
+            return active_rule.attrib["icon"]
+
+        locator = f'.//location[@id="{loc_id}"]/preset'
+        preset = self._domain_objects.find(locator)
+        if preset is not None:
+            return preset.text
+
+    def schemas(self, loc_id):
+        """Obtain the available schemas or schedules based on the location_id."""
+        rule_ids = {}
+        schemas = {}
+        available = []
+        selected = None
+        schedule_temperature = None
+
+        # Legacy schemas
+        if self._smile_legacy:  # Only one schedule allowed
+            name = None
+            for schema in self._domain_objects.findall(".//rule"):
+                rule_name = schema.find("name").text
+                if rule_name:
+                    if "preset" not in rule_name:
+                        name = rule_name
+
+            log_type = "schedule_state"
+            locator = f"appliance[type='thermostat']/logs/point_log[type='{log_type}']/period/measurement"
+            active = False
+            if self._domain_objects.find(locator) is not None:
+                active = self._domain_objects.find(locator).text == "on"
+
+            if name is not None:
+                schemas[name] = active
+
+            available, selected = determine_selected(available, selected, schemas)
+
+            return available, selected, schedule_temperature
+
+        # Current schemas
+        tag = "zone_preset_based_on_time_and_presence_with_override"
+        rule_ids = self.rule_ids_by_tag(tag, loc_id)
+
+        if rule_ids is None:
+            return available, selected, schedule_temperature
+
+        for rule_id, dummy in rule_ids.items():
+            active = False
+            name = self._domain_objects.find(f'rule[@id="{rule_id}"]/name').text
+            if (
+                self._domain_objects.find(f'rule[@id="{rule_id}"]/active').text
+                == "true"
+            ):
+                active = True
+            schemas[name] = active
+            schedules = {}
+            days = {
+                "mo": 0,
+                "tu": 1,
+                "we": 2,
+                "th": 3,
+                "fr": 4,
+                "sa": 5,
+                "su": 6,
+            }
+            locator = f'rule[@id="{rule_id}"]/directives'
+            directives = self._domain_objects.find(locator)
+            for directive in directives:
+                schedule = directive.find("then").attrib
+                keys, dummy = zip(*schedule.items())
+                if str(keys[0]) == "preset":
+                    schedules[directive.attrib["time"]] = float(
+                        self.presets(loc_id)[schedule["preset"]][0]
+                    )
+                else:
+                    schedules[directive.attrib["time"]] = float(schedule["setpoint"])
+
+            for period, temp in schedules.items():
+                moment_1, moment_2 = period.split(",")
+                moment_1 = moment_1.replace("[", "").split(" ")
+                moment_2 = moment_2.replace(")", "").split(" ")
+                result_1 = days.get(moment_1[0], "None")
+                result_2 = days.get(moment_2[0], "None")
+                now = dt.datetime.now().time()
+                start = dt.datetime.strptime(moment_1[1], "%H:%M").time()
+                end = dt.datetime.strptime(moment_2[1], "%H:%M").time()
+                if (
+                    result_1 == dt.datetime.now().weekday()
+                    or result_2 == dt.datetime.now().weekday()
+                ):
+                    if in_between(now, start, end):
+                        schedule_temperature = temp
+
+        available, selected = determine_selected(available, selected, schemas)
+
+        return available, selected, schedule_temperature
+
+    def last_active_schema(self, loc_id):
+        """Determine the last active schema."""
+        epoch = dt.datetime(1970, 1, 1, tzinfo=pytz.utc)
+        rule_ids = {}
+        schemas = {}
+        last_modified = None
+
+        tag = "zone_preset_based_on_time_and_presence_with_override"
+
+        rule_ids = self.rule_ids_by_tag(tag, loc_id)
+        if rule_ids is None:
+            return
+
+        for rule_id, dummy in rule_ids.items():
+            schema_name = self._domain_objects.find(f'rule[@id="{rule_id}"]/name').text
+            schema_date = self._domain_objects.find(
+                f'rule[@id="{rule_id}"]/modified_date'
+            ).text
+            schema_time = parse(schema_date)
+            schemas[schema_name] = (schema_time - epoch).total_seconds()
+
+        if schemas != {}:
+            last_modified = sorted(schemas.items(), key=lambda kv: kv[1])[-1][0]
+
+        return last_modified
+
+    def object_value(self, obj_type, obj_id, measurement):
+        """Obtain the object-value from the thermostat."""
+        search = self._domain_objects
+
+        locator = (
+            f'.//{obj_type}[@id="{obj_id}"]/logs/point_log'
+            f'[type="{measurement}"]/period/measurement'
+        )
+        if search.find(locator) is not None:
+            val = format_measure(search.find(locator).text, None)
+            return val
+
+        return None
diff --git a/plugwise/smile.py b/plugwise/smile.py
index 158a1500d..30b977f7c 100644
--- a/plugwise/smile.py
+++ b/plugwise/smile.py
@@ -1,64 +1,41 @@
 """Plugwise Home Assistant module."""
 import asyncio
-import datetime as dt
 import logging
 
 import aiohttp
-import async_timeout
-from dateutil.parser import parse
-from defusedxml import ElementTree as etree
-
-# Time related
-import pytz
 
 # Version detection
 import semver
 
 from .constants import (
     APPLIANCES,
-    ATTR_NAME,
-    ATTR_TYPE,
-    ATTR_UNIT_OF_MEASUREMENT,
     DEFAULT_PORT,
     DEFAULT_TIMEOUT,
     DEFAULT_USERNAME,
-    DEVICE_MEASUREMENTS,
     DOMAIN_OBJECTS,
-    ENERGY_KILO_WATT_HOUR,
-    ENERGY_WATT_HOUR,
-    HOME_MEASUREMENTS,
     LOCATIONS,
     MODULES,
     NOTIFICATIONS,
-    POWER_WATT,
     RULES,
     SMILES,
     STATUS,
     SWITCH_GROUP_TYPES,
     SYSTEM,
+    THERMOSTAT_CLASSES,
 )
 from .exceptions import (
     ConnectionFailedError,
     DeviceSetupError,
-    DeviceTimeoutError,
-    InvalidAuthentication,
     InvalidXMLError,
-    ResponseError,
     UnsupportedDeviceError,
     XMLDataMissingError,
 )
-from .util import (
-    determine_selected,
-    escape_illegal_xml_characters,
-    format_measure,
-    in_between,
-    version_to_model,
-)
+from .helper import SmileHelper
 
 _LOGGER = logging.getLogger(__name__)
 
 
-class Smile:
+class Smile(SmileHelper):
     """Define the Plugwise object."""
 
     # pylint: disable=too-many-instance-attributes, too-many-public-methods
@@ -73,6 +50,8 @@ def __init__(
         websession: aiohttp.ClientSession = None,
     ):
         """Set the constructor for this class."""
+        super().__init__()
+
         if not websession:
 
             async def _create_session() -> aiohttp.ClientSession:
@@ -90,23 +69,10 @@ async def _create_session() -> aiohttp.ClientSession:
 
         self._auth = aiohttp.BasicAuth(username, password=password)
 
+        self._host = host
+        self._port = port
+        self._endpoint = f"http://{self._host}:{str(self._port)}"
         self._timeout = timeout
-        self._endpoint = f"http://{host}:{str(port)}"
-        self._appliances = None
-        self._domain_objects = None
-        self._home_location = None
-        self._locations = None
-        self._smile_legacy = False
-        self._thermo_master_id = None
-
-        self.active_device_present = False
-        self.gateway_id = None
-        self.heater_id = None
-        self.notifications = {}
-        self.smile_hostname = None
-        self.smile_name = None
-        self.smile_type = None
-        self.smile_version = ()
 
     async def connect(self):
         """Connect to Plugwise device."""
@@ -135,6 +101,8 @@ async def connect(self):
 
         model = version = None
         if gateway is not None:
+            model = result.find(".//gateway/vendor_model").text
+            version = result.find(".//gateway/firmware_version").text
             if gateway.find("hostname") is not None:
                 self.smile_hostname = gateway.find("hostname").text
         else:
@@ -174,10 +142,6 @@ async def connect(self):
                     _LOGGER.error("Connected but no gateway device information found")
                     raise ConnectionFailedError
 
-        if not self._smile_legacy:
-            model = result.find(".//gateway/vendor_model").text
-            version = result.find(".//gateway/firmware_version").text
-
         if model is None or version is None:  # pragma: no cover
             # Corner case check
             _LOGGER.error("Unable to find model or version information")
@@ -190,10 +154,9 @@ async def connect(self):
 
         if target_smile not in SMILES:
             _LOGGER.error(
-                'Your version Smile identified as "%s" \
-                          seems unsupported by our plugin, please create \
-                          an issue on github.com/plugwise/Plugwise-Smile!\
-                          ',
+                '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
@@ -218,578 +181,69 @@ async def close_connection(self):
         """Close the Plugwise connection."""
         await self.websession.close()
 
-    async def request(
-        self,
-        command,
-        retry=3,
-        method="get",
-        data=None,
-        headers=None,
-    ):
-        """Request data."""
-        resp = None
-        url = f"{self._endpoint}{command}"
-
-        try:
-            with async_timeout.timeout(self._timeout):
-                if method == "get":
-                    # Work-around for Stretchv2, should not hurt the other smiles
-                    headers = {"Accept-Encoding": "gzip"}
-                    resp = await self.websession.get(
-                        url, auth=self._auth, headers=headers
-                    )
-                if method == "put":
-                    headers = {"Content-type": "text/xml"}
-                    resp = await self.websession.put(
-                        url, data=data, headers=headers, auth=self._auth
-                    )
-                if method == "delete":
-                    resp = await self.websession.delete(url, auth=self._auth)
-        except asyncio.TimeoutError:
-            if retry < 1:
-                _LOGGER.error("Timed out sending command to Plugwise: %s", command)
-                raise DeviceTimeoutError
-            return await self.request(command, retry - 1)
-
-        # Command accepted gives empty body with status 202
-        if resp.status == 202:
-            return
-        # Cornercase for stretch not responding with 202
-        if method == "put" and resp.status == 200:
-            return
-
-        if resp.status == 401:
-            raise InvalidAuthentication
-
-        result = await resp.text()
-        if not result or "<error>" in result:
-            _LOGGER.error("Smile response empty or error in %s", result)
-            raise ResponseError
-
-        try:
-            # Encode to ensure utf8 parsing
-            xml = etree.XML(escape_illegal_xml_characters(result).encode())
-        except etree.ParseError:
-            _LOGGER.error("Smile returns invalid XML for %s", self._endpoint)
-            raise InvalidXMLError
-
-        return xml
-
-    async def update_appliances(self):
-        """Request appliance data."""
-        new_data = await self.request(APPLIANCES)
-        if new_data is not None:
-            self._appliances = new_data
-
-    async def update_domain_objects(self):
-        """Request domain_objects data."""
-        new_data = await self.request(DOMAIN_OBJECTS)
-        url = f"{self._endpoint}{DOMAIN_OBJECTS}"
-
-        if new_data is not None:
-            self._domain_objects = new_data
-
-        # If Plugwise notifications present:
-        self.notifications = {}
-        notifications = self._domain_objects.findall(".//notification")
-        for notification in notifications:
-            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.info(
-                    "Plugwise notification present but unable to process, manually investigate: %s",
-                    url,
-                )
-
-    async def update_locations(self):
-        """Request locations data."""
-        new_data = await self.request(LOCATIONS)
-        if new_data is not None:
-            self._locations = new_data
-
-    async def update_modules(self):
-        """Request modules data."""
-        new_data = await self.request(MODULES)
-        if new_data is not None:
-            self._modules = new_data
-
     async def full_update_device(self):
         """Update all XML data from device."""
-        # P1 legacy has no appliances
-        if not (self.smile_type == "power" and self._smile_legacy):
-            await self.update_appliances()
-            if self._appliances is None:  # pragma: no cover
-                _LOGGER.error("Appliance data missing")
-                raise XMLDataMissingError
-
         await self.update_domain_objects()
-        if self._domain_objects is None:  # pragma: no cover
-            _LOGGER.error("Domain_objects data missing")
-            raise XMLDataMissingError
-
-        await self.update_locations()
-        if self._locations is None:  # pragma: no cover
-            _LOGGER.error("Locataion data missing")
-            raise XMLDataMissingError
-
-        # Stretch_v2 only uses modules
-        if self.smile_type == "stretch" and self.smile_version[1].major == 2:
-            await self.update_modules()
-            if self._modules is None:  # pragma: no cover
-                _LOGGER.error("Modules data missing")
-                raise XMLDataMissingError
-
-    @staticmethod
-    def _types_finder(data):
-        """Detect types within locations from logs."""
-        types = set()
-        for measure, attrs in HOME_MEASUREMENTS.items():
-            locator = f".//logs/point_log[type='{measure}']"
-            if data.find(locator) is not None:
-                log = data.find(locator)
-
-                if measure == "outdoor_temperature":
-                    types.add(attrs[ATTR_TYPE])
-
-                p_locator = ".//electricity_point_meter"
-                if log.find(p_locator) is not None:
-                    if log.find(p_locator).get("id"):
-                        types.add(attrs[ATTR_TYPE])
-
-        return types
-
-    def get_all_appliances(self):
-        """Determine available appliances from inventory."""
-        appliances = {}
-        stretch_v2 = self.smile_type == "stretch" and self.smile_version[1].major == 2
-        stretch_v3 = self.smile_type == "stretch" and self.smile_version[1].major == 3
-
-        locations, home_location = self.get_all_locations()
-
-        if self._smile_legacy and self.smile_type == "power":
-            # Inject home_location as dev_id for legacy so
-            # get_appliance_data can use loc_id for dev_id.
-            appliances[self._home_location] = {
-                "name": "P1",
-                "model": "Smile P1",
-                "types": {"power", "home"},
-                "class": "gateway",
-                "location": home_location,
-            }
-            self.gateway_id = self._home_location
-
-            return appliances
-
-        # TODO: add locations with members as appliance as well
-        # example 'electricity consumed/produced and relay' on Adam
-        # Basically walk locations for 'members' not set[] and
-        # scan for the same functionality
-
-        # Find gateway and heater devices
-        for appliance in self._appliances:
-            if appliance.find("type").text == "gateway":
-                self.gateway_id = appliance.attrib["id"]
-            if appliance.find("type").text == "heater_central":
-                self.heater_id = appliance.attrib["id"]
-
-        # for legacy it is the same device
-        if self._smile_legacy and self.smile_type == "thermostat":
-            self.gateway_id = self.heater_id
-
-        for appliance in self._appliances:
-            appliance_location = None
-            appliance_types = set()
-
-            appliance_id = appliance.attrib["id"]
-            appliance_class = appliance.find("type").text
-            appliance_descr = appliance.find("description").text
-            appliance_name = appliance.find("name").text
-            appliance_model = appliance_class.replace("_", " ").title()
-            if stretch_v2:
-                appl_search = appliance.find(".//services/electricity_point_meter")
-                if appl_search is not None:
-                    appl_serv_epm_id = appl_search.attrib["id"]
-                    module = self._modules.find(
-                        f".//electricity_point_meter[@id='{appl_serv_epm_id}']...."
-                    )
-                    hw_version = module.find("hardware_version").text.replace("-", "")
-                    appliance_model = version_to_model(hw_version)
-
-            if stretch_v3:
-                appliance_model = appliance_descr
-
-            # Nothing useful in opentherm so skip it
-            if appliance_class == "open_therm_gateway":
-                continue
-
-            # Appliance with location (i.e. a device)
-            if appliance.find("location") is not None:
-                appliance_location = appliance.find("location").attrib["id"]
-                for appl_type in self._types_finder(appliance):
-                    appliance_types.add(appl_type)
-            else:
-                # Return all types applicable to home
-                appliance_types = locations[home_location]["types"]
-                # If heater or gatweay override registering
-                if appliance_class == "heater_central":
-                    appliance_id = self.heater_id
-                    appliance_name = self.smile_name
-                if appliance_class == "gateway":
-                    appliance_id = self.gateway_id
-                    appliance_name = self.smile_name
-
-            # Determine appliance_type from functionality
-            if (
-                appliance.find(".//actuator_functionalities/relay_functionality")
-                is not None
-                or appliance.find(".//actuators/relay") is not None
-            ):
-                appliance_types.add("plug")
-            elif (
-                appliance.find(".//actuator_functionalities/thermostat_functionality")
-                is not None
-            ):
-                appliance_types.add("thermostat")
-
-            if self.smile_type != "stretch" and "plug" in appliance_types:
-                appliance_model = "Plug"
-
-            if appliance_model == "Gateway":
-                appliance_model = f"Smile {self.smile_name}"
-
-            appliances[appliance_id] = {
-                "name": appliance_name,
-                "model": appliance_model,
-                "types": appliance_types,
-                "class": appliance_class,
-                "location": appliance_location,
-            }
-
-        return appliances
-
-    def get_all_locations(self):
-        """Determine available locations from inventory."""
-        home_location = None
-        locations = {}
-
-        # Legacy Anna without outdoor_temp and Stretches have no locations, create one containing all appliances
-        if len(self._locations) == 0 and self._smile_legacy:
-            appliances = set()
-            home_location = 0
-
-            # Add Anna appliances
-            for appliance in self._appliances:
-                appliances.add(appliance.attrib["id"])
-
-            if self.smile_type == "thermostat":
-                locations[0] = {
-                    "name": "Legacy Anna",
-                    "types": {"temperature"},
-                    "members": appliances,
-                }
-            if self.smile_type == "stretch":
-                locations[0] = {
-                    "name": "Legacy Stretch",
-                    "types": {"power"},
-                    "members": appliances,
-                }
-
-            self._home_location = home_location
-
-            return locations, home_location
-
-        for location in self._locations:
-            location_name = location.find("name").text
-            location_id = location.attrib["id"]
-            location_types = set()
-            location_members = set()
-
-            # Group of appliances
-            locator = ".//appliances/appliance"
-            if location.find(locator) is not None:
-                for member in location.findall(locator):
-                    location_members.add(member.attrib["id"])
-
-            if location_name == "Home":
-                home_location = location_id
-                location_types.add("home")
-
-                for location_type in self._types_finder(location):
-                    location_types.add(location_type)
-
-            # Legacy P1 right location has 'services' filled
-            # test data has 5 for example
-            locator = ".//services"
-            if (
-                self._smile_legacy
-                and self.smile_type == "power"
-                and len(location.find(locator)) > 0
-            ):
-                # Override location name found to match
-                location_name = "Home"
-                home_location = location_id
-                location_types.add("home")
-                location_types.add("power")
-
-            locations[location_id] = {
-                "name": location_name,
-                "types": location_types,
-                "members": location_members,
-            }
-
-        self._home_location = home_location
-
-        return locations, home_location
+        self._locations = await self.request(LOCATIONS)
 
-    def single_master_thermostat(self):
-        """Determine if there is a single master thermostat in the setup."""
-        count = 0
-        locations, dummy = self.scan_thermostats()
-        for dummy, data in locations.items():
-            if "master_prio" in data:
-                if data.get("master_prio") > 0:
-                    count += 1
-
-        if count == 0:
-            return None
-        if count == 1:
-            return True
-        return False
-
-    def scan_thermostats(self, debug_text="missing text"):
-        """Update locations with actual master/slave thermostats."""
-        locations, home_location = self.match_locations()
-        appliances = self.get_all_appliances()
-
-        thermo_matching = {
-            "thermostat": 3,
-            "zone_thermostat": 2,
-            "thermostatic_radiator_valve": 1,
-        }
-
-        high_prio = 0
-        for loc_id, location_details in locations.items():
-            locations[loc_id] = location_details
-
-            if "thermostat" in location_details["types"] and loc_id != home_location:
-                locations[loc_id].update(
-                    {"master": None, "master_prio": 0, "slaves": set()}
-                )
-            elif loc_id == home_location and self._smile_legacy:
-                locations[loc_id].update(
-                    {"master": None, "master_prio": 0, "slaves": set()}
-                )
-            else:  # pragma: no cover
-                continue
-
-            for appliance_id, appliance_details in appliances.items():
-
-                appl_class = appliance_details["class"]
-                if (
-                    loc_id == appliance_details["location"]
-                    or (self._smile_legacy and not appliance_details["location"])
-                ) and appl_class in thermo_matching:
-
-                    # Pre-elect new master
-                    if thermo_matching[appl_class] > locations[loc_id]["master_prio"]:
-
-                        # Demote former master
-                        if locations[loc_id]["master"] is not None:
-                            locations[loc_id]["slaves"].add(locations[loc_id]["master"])
-
-                        # Crown master
-                        locations[loc_id]["master_prio"] = thermo_matching[appl_class]
-                        locations[loc_id]["master"] = appliance_id
-
-                    else:
-                        locations[loc_id]["slaves"].add(appliance_id)
-
-                # Find highest ranking thermostat
-                if appl_class in thermo_matching:
-                    if thermo_matching[appl_class] > high_prio:
-                        high_prio = thermo_matching[appl_class]
-                        self._thermo_master_id = appliance_id
-
-            if locations[loc_id]["master"] is None:
-                _LOGGER.debug(
-                    "Location %s has no (master) thermostat", location_details["name"]
-                )
-
-        # Return location including slaves
-        return locations, home_location
-
-    def match_locations(self):
-        """Update locations with used types of appliances."""
-        match_locations = {}
-
-        locations, home_location = self.get_all_locations()
-        appliances = self.get_all_appliances()
+        # P1 legacy has no appliances
+        if not (self.smile_type == "power" and self._smile_legacy):
+            self._appliances = await self.request(APPLIANCES)
 
-        for location_id, location_details in locations.items():
-            for dummy, appliance_details in appliances.items():
-                if appliance_details["location"] == location_id:
-                    for appl_type in appliance_details["types"]:
-                        location_details["types"].add(appl_type)
+        # No need to import modules for P1, no userfull info
+        if self.smile_type != "power":
+            self._modules = await self.request(MODULES)
 
-            match_locations[location_id] = location_details
+    async def update_device(self):
+        """Update all XML data from device."""
+        await self.update_domain_objects()
 
-        return match_locations, home_location
+        # P1 legacy has no appliances
+        if not (self.smile_type == "power" and self._smile_legacy):
+            self._appliances = await self.request(APPLIANCES)
 
     def get_all_devices(self):
         """Determine available devices from inventory."""
         devices = {}
+        self.scan_thermostats()
 
-        appliances = self.get_all_appliances()
-        thermo_locations, home_location = self.scan_thermostats()
-
-        for appliance, details in appliances.items():
+        for appliance, details in self.appl_data.items():
             loc_id = details["location"]
             if loc_id is None:
-                details["location"] = home_location
+                details["location"] = self._home_location
 
             # Override slave thermostat class
-            if loc_id in thermo_locations:
-                if "slaves" in thermo_locations[loc_id]:
-                    if appliance in thermo_locations[loc_id]["slaves"]:
+            if loc_id in self.thermo_locs:
+                if "slaves" in self.thermo_locs[loc_id]:
+                    if appliance in self.thermo_locs[loc_id]["slaves"]:
                         details["class"] = "thermo_sensor"
 
-            if details["name"] == "Anna" and not self.single_master_thermostat():
-                details["model"] = "Zone Thermostat"
-
             devices[appliance] = details
 
-        group_data = self.get_group_switches()
+        group_data = self.group_switches()
         if group_data is not None:
             devices.update(group_data)
 
         return devices
 
-    def get_group_switches(self):
-        """Provide switching- or pump-groups, from DOMAIN_OBJECTS."""
-        switch_groups = {}
-        search = self._domain_objects
-
-        appliances = search.findall("./appliance")
-        groups = search.findall("./group")
-
-        for group in groups:
-            group_appl = {}
-            members = []
-            group_id = group.attrib["id"]
-            group_name = group.find("name").text
-            group_type = group.find("type").text
-            if self.smile_type == "stretch":
-                group_appliance = group.findall("appliances/appliance")
-                for dummy in group_appliance:
-                    members.append(dummy.attrib["id"])
-            else:
-                for appliance in appliances:
-                    if appliance.find("./groups/group") is not None:
-                        appl_id = appliance.attrib["id"]
-                        apl_gr_id = appliance.find("./groups/group").attrib["id"]
-                        if apl_gr_id == group_id:
-                            members.append(appl_id)
-
-            if group_type in SWITCH_GROUP_TYPES:
-                group_appl[group_id] = {
-                    "name": group_name,
-                    "model": "group_switch",
-                    "types": {"switch_group"},
-                    "class": group_type,
-                    "members": members,
-                    "location": None,
-                }
-
-            switch_groups.update(group_appl)
-
-        return switch_groups
-
-    def get_open_valves(self):
-        """Obtain the amount of open valves, from APPLIANCES."""
-        appliances = self._appliances.findall(".//appliance")
-
-        open_valve_count = 0
-        for appliance in appliances:
-            locator = './/logs/point_log[type="valve_position"]/period/measurement'
-            if appliance.find(locator) is not None:
-                measure = appliance.find(locator).text
-                if float(measure) > 0.0:
-                    open_valve_count += 1
-
-        return open_valve_count
-
     def get_device_data(self, dev_id):
         """Provide device-data, based on location_id, from APPLIANCES."""
         devices = self.get_all_devices()
         details = devices.get(dev_id)
-
-        thermostat_classes = [
-            "thermostat",
-            "zone_thermostat",
-            "thermostatic_radiator_valve",
-        ]
-
-        device_data = self.get_appliance_data(dev_id)
-
-        # Legacy_anna: create heating_state and leave out domestic_hot_water_state
-        if "boiler_state" in device_data:
-            device_data["heating_state"] = device_data["intended_boiler_state"]
-            device_data.pop("boiler_state", None)
-            device_data.pop("intended_boiler_state", None)
-
-        # Fix for Adam + Anna: heating_state also present under Anna, remove
-        if "thermostat" in device_data:
-            device_data.pop("heating_state", None)
-
-        # Adam: indicate heating_state based on valves being open in case of city-provided heating
-        if self.smile_name == "Adam":
-            if details["class"] == "heater_central":
-                if not self.active_device_present:
-                    device_data["heating_state"] = True
-                    if self.get_open_valves() == 0:
-                        device_data["heating_state"] = False
-
-        # Anna, Lisa, Tom/Floor
-        if details["class"] in thermostat_classes:
-            device_data["active_preset"] = self.get_preset(details["location"])
-            device_data["presets"] = self.get_presets(details["location"])
-
-            avail_schemas, sel_schema, sched_setpoint = self.get_schemas(
-                details["location"]
-            )
-            if not self._smile_legacy:
-                device_data["schedule_temperature"] = sched_setpoint
-            device_data["available_schedules"] = avail_schemas
-            device_data["selected_schedule"] = sel_schema
-            if self._smile_legacy:
-                device_data["last_used"] = "".join(map(str, avail_schemas))
-            else:
-                device_data["last_used"] = self.get_last_active_schema(
-                    details["location"]
-                )
-
-        # Anna specific
-        if details["class"] in ["thermostat"]:
-            illuminance = self.get_object_value("appliance", dev_id, "illuminance")
-            if illuminance is not None:
-                device_data["illuminance"] = illuminance
+        device_data = self.appliance_data(dev_id)
 
         # Generic
         if details["class"] == "gateway" or dev_id == self.gateway_id:
             # Anna: outdoor_temperature only present in domain_objects
             if "outdoor_temperature" not in device_data:
-                outdoor_temperature = self.get_object_value(
+                outdoor_temperature = self.object_value(
                     "location", self._home_location, "outdoor_temperature"
                 )
                 if outdoor_temperature is not None:
                     device_data["outdoor_temperature"] = outdoor_temperature
 
             # Try to get P1 data and 2nd outdoor_temperature, when present
-            power_data = self.get_power_data_from_location(details["location"])
+            power_data = self.power_data_from_location(details["location"])
             if power_data is not None:
                 device_data.update(power_data)
 
@@ -797,7 +251,7 @@ def get_device_data(self, dev_id):
         if details["class"] in SWITCH_GROUP_TYPES:
             counter = 0
             for member in details["members"]:
-                appl_data = self.get_appliance_data(member)
+                appl_data = self.appliance_data(member)
                 if appl_data["relay"]:
                     counter += 1
 
@@ -805,337 +259,60 @@ def get_device_data(self, dev_id):
             if counter == 0:
                 device_data["relay"] = False
 
-        return device_data
-
-    def get_appliance_data(self, dev_id):
-        """
-        Obtain the appliance-data connected to a location.
-
-        Determined from APPLIANCES or legacy DOMAIN_OBJECTS.
-        """
-        data = {}
-        search = self._appliances
-
-        if self._smile_legacy and self.smile_type != "stretch":
-            search = self._domain_objects
-
-        appliances = search.findall(f'.//appliance[@id="{dev_id}"]')
-
-        for appliance in appliances:
-            for measurement, attrs in DEVICE_MEASUREMENTS.items():
-
-                p_locator = (
-                    f'.//logs/point_log[type="{measurement}"]/period/measurement'
-                )
-                if appliance.find(p_locator) is not None:
-                    if self._smile_legacy:
-                        if measurement == "domestic_hot_water_state":
-                            continue
-
-                    measure = appliance.find(p_locator).text
-                    # Fix for Adam + Anna: there is a pressure-measurement with an unrealistic value,
-                    # this measurement appears at power-on and is never updated, therefore remove.
-                    if (
-                        measurement == "central_heater_water_pressure"
-                        and float(measure) > 3.5
-                    ):
-                        continue
-                    # The presence of either indicates a local active device, e.g. heat-pump or gas-fired heater
-                    if measurement in ["compressor_state", "flame_state"]:
-                        self.active_device_present = True
+        # Legacy_anna: create Auxiliary heating_state and leave out domestic_hot_water_state
+        if "boiler_state" in device_data:
+            device_data["heating_state"] = device_data["intended_boiler_state"]
+            device_data.pop("boiler_state", None)
+            device_data.pop("intended_boiler_state", None)
 
-                    try:
-                        measurement = attrs[ATTR_NAME]
-                    except KeyError:
-                        measurement = measurement
+        # Adam: indicate heating_state based on valves being open in case of city-provided heating
+        if self.smile_name == "Adam":
+            if details["class"] == "gateway":
+                if not self.active_device_present and self.heating_valves() is not None:
+                    device_data["heating_state"] = True
+                    if self.heating_valves() == 0:
+                        device_data["heating_state"] = False
 
-                    data[measurement] = format_measure(
-                        measure, attrs[ATTR_UNIT_OF_MEASUREMENT]
-                    )
+        if details["class"] not in THERMOSTAT_CLASSES:
+            return device_data
 
-                i_locator = (
-                    f'.//logs/interval_log[type="{measurement}"]/period/measurement'
-                )
-                if appliance.find(i_locator) is not None:
-                    name = f"{measurement}_interval"
-                    measure = appliance.find(i_locator).text
-
-                    data[name] = format_measure(measure, ENERGY_WATT_HOUR)
-
-        return data
-
-    def get_power_data_from_location(self, loc_id):
-        """Obtain the power-data from domain_objects based on location."""
-        direct_data = {}
-        search = self._domain_objects
-        t_string = "tariff"
-        if self._smile_legacy and self.smile_type == "power":
-            t_string = "tariff_indicator"
-
-        loc_logs = search.find(f'.//location[@id="{loc_id}"]/logs')
-
-        if loc_logs is None:
-            return
-
-        log_list = ["point_log", "cumulative_log", "interval_log"]
-        peak_list = ["nl_peak", "nl_offpeak"]
-
-        # meter_string = ".//{}[type='{}']/"
-        for measurement, attrs in HOME_MEASUREMENTS.items():
-            for log_type in log_list:
-                for peak_select in peak_list:
-                    locator = (
-                        f'.//{log_type}[type="{measurement}"]/period/'
-                        f'measurement[@{t_string}="{peak_select}"]'
-                    )
-                    # Only once try to find P1 Legacy values
-                    if loc_logs.find(locator) is None and self.smile_type == "power":
-                        locator = (
-                            f'.//{log_type}[type="{measurement}"]/period/measurement'
-                        )
-
-                        # Skip peak if not split (P1 Legacy)
-                        if peak_select == "nl_offpeak":
-                            continue
-
-                    if loc_logs.find(locator) is None:
-                        continue
-
-                    peak = peak_select.split("_")[1]
-                    if peak == "offpeak":
-                        peak = "off_peak"
-                    log_found = log_type.split("_")[0]
-                    key_string = f"{measurement}_{peak}_{log_found}"
-                    net_string = f"net_electricity_{log_found}"
-                    val = loc_logs.find(locator).text
-                    f_val = format_measure(val, attrs[ATTR_UNIT_OF_MEASUREMENT])
-                    # Format only HOME_MEASUREMENT POWER_WATT values, do not move to util-format_meaure function!
-                    if attrs[ATTR_UNIT_OF_MEASUREMENT] == POWER_WATT:
-                        f_val = int(round(float(val)))
-                    if all(
-                        item in key_string for item in ["electricity", "cumulative"]
-                    ):
-                        f_val = format_measure(val, ENERGY_KILO_WATT_HOUR)
-                    # Energy differential
-                    if "electricity" in measurement:
-                        diff = 1
-                        if "produced" in measurement:
-                            diff = -1
-                        if net_string not in direct_data:
-                            direct_data[net_string] = 0
-                        if isinstance(f_val, int):
-                            direct_data[net_string] += f_val * diff
-                        else:
-                            direct_data[net_string] += float(f_val * diff)
-
-                    if "gas" in measurement:
-                        key_string = f"{measurement}_{log_found}"
-
-                    direct_data[key_string] = f_val
-
-        if direct_data != {}:
-            return direct_data
-
-    def get_preset(self, loc_id):
-        """
-        Obtain the active preset based on the location_id.
+        # Anna, Lisa, Tom/Floor
+        device_data["active_preset"] = self.preset(details["location"])
+        device_data["presets"] = self.presets(details["location"])
 
-        Determined from DOMAIN_OBJECTS.
-        """
+        avail_schemas, sel_schema, sched_setpoint = self.schemas(details["location"])
+        if not self._smile_legacy:
+            device_data["schedule_temperature"] = sched_setpoint
+        device_data["available_schedules"] = avail_schemas
+        device_data["selected_schedule"] = sel_schema
         if self._smile_legacy:
-            active_rule = self._domain_objects.find(
-                "rule[active='true']/directives/when/then"
-            )
-            if active_rule is None or "icon" not in active_rule.keys():
-                return
-            return active_rule.attrib["icon"]
+            device_data["last_used"] = "".join(map(str, avail_schemas))
+        else:
+            device_data["last_used"] = self.last_active_schema(details["location"])
 
-        locator = f'.//location[@id="{loc_id}"]/preset'
-        preset = self._domain_objects.find(locator)
-        if preset is not None:
-            return preset.text
+        # Anna specific
+        illuminance = self.object_value("appliance", dev_id, "illuminance")
+        if illuminance is not None:
+            device_data["illuminance"] = illuminance
 
-    def get_presets(self, loc_id):
-        """Get the presets from the thermostat based on location_id."""
-        presets = {}
-        tag = "zone_setpoint_and_state_based_on_preset"
+        return device_data
 
-        if self._smile_legacy:
-            return self.__get_presets_legacy()
-
-        rule_ids = self.get_rule_ids_by_tag(tag, loc_id)
-        if rule_ids is None:
-            rule_ids = self.get_rule_ids_by_name("Thermostat presets", loc_id)
-            if rule_ids is None:
-                return presets
-
-        for rule_id in rule_ids:
-            directives = self._domain_objects.find(f'rule[@id="{rule_id}"]/directives')
-
-            for directive in directives:
-                preset = directive.find("then").attrib
-                keys, dummy = zip(*preset.items())
-                if str(keys[0]) == "setpoint":
-                    presets[directive.attrib["preset"]] = [float(preset["setpoint"]), 0]
-                else:
-                    presets[directive.attrib["preset"]] = [
-                        float(preset["heating_setpoint"]),
-                        float(preset["cooling_setpoint"]),
-                    ]
-
-        return presets
-
-    def get_schemas(self, loc_id):
-        """Obtain the available schemas or schedules based on the location_id."""
-        rule_ids = {}
-        schemas = {}
-        available = []
-        selected = None
-        schedule_temperature = None
-
-        # Legacy schemas
-        if self._smile_legacy:  # Only one schedule allowed
-            name = None
-            for schema in self._domain_objects.findall(".//rule"):
-                rule_name = schema.find("name").text
-                if rule_name:
-                    if "preset" not in rule_name:
-                        name = rule_name
-
-            log_type = "schedule_state"
-            locator = f"appliance[type='thermostat']/logs/point_log[type='{log_type}']/period/measurement"
-            active = False
-            if self._domain_objects.find(locator) is not None:
-                active = self._domain_objects.find(locator).text == "on"
-
-            if name is not None:
-                schemas[name] = active
-
-            available, selected = determine_selected(available, selected, schemas)
-
-            return available, selected, schedule_temperature
-
-        # Current schemas
-        tag = "zone_preset_based_on_time_and_presence_with_override"
-        rule_ids = self.get_rule_ids_by_tag(tag, loc_id)
-
-        if rule_ids is None:
-            return available, selected, schedule_temperature
-
-        for rule_id, dummy in rule_ids.items():
-            active = False
-            name = self._domain_objects.find(f'rule[@id="{rule_id}"]/name').text
-            if (
-                self._domain_objects.find(f'rule[@id="{rule_id}"]/active').text
-                == "true"
-            ):
-                active = True
-            schemas[name] = active
-            schedules = {}
-            days = {
-                "mo": 0,
-                "tu": 1,
-                "we": 2,
-                "th": 3,
-                "fr": 4,
-                "sa": 5,
-                "su": 6,
-            }
-            locator = f'rule[@id="{rule_id}"]/directives'
-            directives = self._domain_objects.find(locator)
-            for directive in directives:
-                schedule = directive.find("then").attrib
-                keys, dummy = zip(*schedule.items())
-                if str(keys[0]) == "preset":
-                    schedules[directive.attrib["time"]] = float(
-                        self.get_presets(loc_id)[schedule["preset"]][0]
-                    )
-                else:
-                    schedules[directive.attrib["time"]] = float(schedule["setpoint"])
-
-            for period, temp in schedules.items():
-                moment_1, moment_2 = period.split(",")
-                moment_1 = moment_1.replace("[", "").split(" ")
-                moment_2 = moment_2.replace(")", "").split(" ")
-                result_1 = days.get(moment_1[0], "None")
-                result_2 = days.get(moment_2[0], "None")
-                now = dt.datetime.now().time()
-                start = dt.datetime.strptime(moment_1[1], "%H:%M").time()
-                end = dt.datetime.strptime(moment_2[1], "%H:%M").time()
-                if (
-                    result_1 == dt.datetime.now().weekday()
-                    or result_2 == dt.datetime.now().weekday()
-                ):
-                    if in_between(now, start, end):
-                        schedule_temperature = temp
-
-        available, selected = determine_selected(available, selected, schemas)
-
-        return available, selected, schedule_temperature
-
-    def get_last_active_schema(self, loc_id):
-        """Determine the last active schema."""
-        epoch = dt.datetime(1970, 1, 1, tzinfo=pytz.utc)
-        rule_ids = {}
-        schemas = {}
-        last_modified = None
-
-        tag = "zone_preset_based_on_time_and_presence_with_override"
-
-        rule_ids = self.get_rule_ids_by_tag(tag, loc_id)
-        if rule_ids is None:
-            return
-
-        for rule_id, dummy in rule_ids.items():
-            schema_name = self._domain_objects.find(f'rule[@id="{rule_id}"]/name').text
-            schema_date = self._domain_objects.find(
-                f'rule[@id="{rule_id}"]/modified_date'
-            ).text
-            schema_time = parse(schema_date)
-            schemas[schema_name] = (schema_time - epoch).total_seconds()
-
-        if schemas != {}:
-            last_modified = sorted(schemas.items(), key=lambda kv: kv[1])[-1][0]
-
-        return last_modified
-
-    def get_rule_ids_by_tag(self, tag, loc_id):
-        """Obtain the rule_id based on the given template_tag and location_id."""
-        schema_ids = {}
-        locator1 = f'.//template[@tag="{tag}"]'
-        locator2 = f'.//contexts/context/zone/location[@id="{loc_id}"]'
-        for rule in self._domain_objects.findall(".//rule"):
-            if rule.find(locator1) is not None:
-                if rule.find(locator2) is not None:
-                    schema_ids[rule.attrib["id"]] = loc_id
-
-        if schema_ids != {}:
-            return schema_ids
-
-    def get_rule_ids_by_name(self, name, loc_id):
-        """Obtain the rule_id on the given name and location_id."""
-        schema_ids = {}
-        locator = f'.//contexts/context/zone/location[@id="{loc_id}"]'
-        for rule in self._domain_objects.findall(f'.//rule[name="{name}"]'):
-            if rule.find(locator) is not None:
-                schema_ids[rule.attrib["id"]] = loc_id
-
-        if schema_ids != {}:
-            return schema_ids
-
-    def get_object_value(self, obj_type, obj_id, measurement):
-        """Obtain the object-value from the thermostat."""
-        search = self._domain_objects
-
-        locator = (
-            f'.//{obj_type}[@id="{obj_id}"]/logs/point_log'
-            f'[type="{measurement}"]/period/measurement'
-        )
-        if search.find(locator) is not None:
-            val = format_measure(search.find(locator).text, None)
-            return val
+    def single_master_thermostat(self):
+        """Determine if there is a single master thermostat in the setup."""
+        if self.smile_type != "thermostat":
+            self.thermo_locs = self.match_locations()
+            return None
 
-        return None
+        count = 0
+        self.scan_thermostats()
+        for dummy, data in self.thermo_locs.items():
+            if "master_prio" in data:
+                if data.get("master_prio") > 0:
+                    count += 1
+
+        if count == 1:
+            return True
+        return False
 
     async def set_schedule_state(self, loc_id, name, state):
         """
@@ -1146,7 +323,7 @@ async def set_schedule_state(self, loc_id, name, state):
         if self._smile_legacy:
             return await self.set_schedule_state_legacy(name, state)
 
-        schema_rule_ids = self.get_rule_ids_by_name(str(name), loc_id)
+        schema_rule_ids = self.rule_ids_by_name(str(name), loc_id)
         if schema_rule_ids == {} or schema_rule_ids is None:
             return False
 
@@ -1178,7 +355,7 @@ async def set_preset(self, loc_id, preset):
         location_name = current_location.find("name").text
         location_type = current_location.find("type").text
 
-        if preset not in self.get_presets(loc_id):
+        if preset not in self.presets(loc_id):
             return False
 
         uri = f"{LOCATIONS};id={loc_id}"
@@ -1194,7 +371,7 @@ async def set_preset(self, loc_id, preset):
     async def set_temperature(self, loc_id, temperature):
         """Send temperature-set request to the locations thermostat."""
         temperature = str(temperature)
-        uri = self.__get_temperature_uri(loc_id)
+        uri = self.temperature_uri(loc_id)
         data = (
             "<thermostat_functionality><setpoint>"
             f"{temperature}</setpoint></thermostat_functionality>"
@@ -1203,16 +380,6 @@ async def set_temperature(self, loc_id, temperature):
         await self.request(uri, method="put", data=data)
         return True
 
-    def __get_temperature_uri(self, loc_id):
-        """Determine the location-set_temperature uri - from LOCATIONS."""
-        if self._smile_legacy:
-            return self.__get_temperature_uri_legacy()
-
-        locator = f'location[@id="{loc_id}"]/actuator_functionalities/thermostat_functionality'
-        thermostat_functionality_id = self._locations.find(locator).attrib["id"]
-
-        return f"{LOCATIONS};id={loc_id}/thermostat;id={thermostat_functionality_id}"
-
     async def set_switch_state(self, appl_id, members, model, state):
         """Switch the Switch off/on."""
         actuator = "actuator_functionalities"
@@ -1250,21 +417,6 @@ async def set_switch_state(self, appl_id, members, model, state):
         await self.request(uri, method="put", data=data)
         return True
 
-    # LEGACY Anna functions
-
-    def __get_presets_legacy(self):
-        """Get presets from domain_objects for legacy Smile."""
-        preset_dictionary = {}
-        for directive in self._domain_objects.findall("rule/directives/when/then"):
-            if directive is not None and "icon" in directive.keys():
-                # Ensure list of heating_setpoint, cooling_setpoint
-                preset_dictionary[directive.attrib["icon"]] = [
-                    float(directive.attrib["temperature"]),
-                    0,
-                ]
-
-        return preset_dictionary
-
     async def set_preset_legacy(self, preset):
         """Set the given preset on the thermostat - from DOMAIN_OBJECTS."""
         locator = f'rule/directives/when/then[@icon="{preset}"].../.../...'
@@ -1278,13 +430,6 @@ async def set_preset_legacy(self, preset):
         await self.request(uri, method="put", data=data)
         return True
 
-    def __get_temperature_uri_legacy(self):
-        """Determine the location-set_temperature uri - from APPLIANCES."""
-        locator = ".//appliance[type='thermostat']"
-        appliance_id = self._appliances.find(locator).attrib["id"]
-
-        return f"{APPLIANCES};id={appliance_id}/thermostat"
-
     async def set_schedule_state_legacy(self, name, state):
         """Send a set request to the schema with the given name."""
         schema_rule_id = None
diff --git a/plugwise/util.py b/plugwise/util.py
index 2c46ac3a5..85563b83f 100644
--- a/plugwise/util.py
+++ b/plugwise/util.py
@@ -36,13 +36,16 @@ def validate_mac(mac):
 
 def version_to_model(version):
     """Translate hardware_version to device type."""
-    model = HW_MODELS.get(version[4:10], None)
+    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],
-            None,
-        )
+        model = HW_MODELS.get(version[-2:] + version[-4:-2] + version[-6:-4])
+
     return model if model is not None else "Unknown"
 
 
diff --git a/userdata/adam_plus_anna_new/core.modules.xml b/userdata/adam_plus_anna_new/core.modules.xml
new file mode 100644
index 000000000..5eebb9402
--- /dev/null
+++ b/userdata/adam_plus_anna_new/core.modules.xml
@@ -0,0 +1,390 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<modules>
+	<module id='c3b7fd85480c42dca8e7adc1ea82f14d'>
+		<vendor_name>Telegesis</vendor_name>
+		<vendor_model>ETRX3USB</vendor_model>
+		<hardware_version></hardware_version>
+		<firmware_version></firmware_version>
+		<created_date>2019-08-15T16:18:41.530+02:00</created_date>
+		<modified_date>2019-08-15T16:18:41.619+02:00</modified_date>
+		<deleted_date></deleted_date>
+		<services/>
+		<protocols>
+			<zig_bee_coordinator id='fcd3478dede94c5c87b5ea39a3404708'>
+				<mac_address>0123456789AB</mac_address>
+				<permit_joining>false</permit_joining>
+				<neighbors><zig_bee_node id='0374084c2e2843f19688e289235859ce'/><zig_bee_node id='9074cd5161764e8eb0261d5ba99a0428'/><zig_bee_node id='9ca5df0038564aeeb59709ab3de23f97'/><zig_bee_node id='a7c4a564cd4943a1ab9eee428270b2ae'/></neighbors>
+				<zig_bee_nodes><zig_bee_node id='0374084c2e2843f19688e289235859ce'/><zig_bee_node id='87f66dac0b92499897598c95fe55286e'/><zig_bee_node id='9074cd5161764e8eb0261d5ba99a0428'/><zig_bee_node id='9ca5df0038564aeeb59709ab3de23f97'/><zig_bee_node id='a7c4a564cd4943a1ab9eee428270b2ae'/></zig_bee_nodes>
+			</zig_bee_coordinator>
+		</protocols>
+	</module>
+	<module id='bb2fc3da88144d018ee51861e13f505d'>
+		<vendor_name>Plugwise B.V.</vendor_name>
+		<vendor_model>143.1</vendor_model>
+		<hardware_version></hardware_version>
+		<firmware_version></firmware_version>
+		<created_date>2020-02-22T15:33:28.039+01:00</created_date>
+		<modified_date>2020-02-22T15:33:52.388+01:00</modified_date>
+		<deleted_date></deleted_date>
+		<services>
+			<boiler_state id='3ccb01f757d54f2da0f4b96c49b666a5' log_type='intended_domestic_hot_water_comfort_mode'>
+				<functionalities><point_log id='2be8d0b8719e438b8c671faedc2eb808'/></functionalities>
+			</boiler_state>
+			<boiler_state id='b84508588e7446138f2413f3eee9adda' log_type='intended_central_heating_state'>
+				<functionalities><point_log id='982347f1f5f5482ea95bed3bf3e4d8d1'/></functionalities>
+			</boiler_state>
+			<thermo_meter id='c25df1471fbd42cbaedd3c54525d2a13' log_type='temperature'>
+				<functionalities><point_log id='467502851205436ca6a53ab2cfc7e495'/></functionalities>
+			</thermo_meter>
+			<thermostat id='dc9f38dd89014dbeab207939aae3b375' log_type='thermostat'>
+				<functionalities><thermostat_functionality id='b649d86973f8472abb9bca15d2b9ba18'/><point_log id='b7400fdd32654c45a8d6c41dbe8f457e'/></functionalities>
+			</thermostat>
+		</services>
+		<protocols>
+			<open_therm_thermostat id='a56dbe7454c543f7a473c4e4d9a9f30f'>
+				<open_therm_version>0.0</open_therm_version>
+				<open_therm_gateway id='81dacf5db3b94bfd9bdeef364fc07234'/>
+			</open_therm_thermostat>
+		</protocols>
+	</module>
+	<module id='595ebfde620542e784a58b357b7d8ed7'>
+		<vendor_name>Plugwise</vendor_name>
+		<vendor_model>106-03</vendor_model>
+		<hardware_version>1</hardware_version>
+		<firmware_version>2020-10-06T02:00:00+02:00</firmware_version>
+		<upgrade/>
+		<created_date>2020-04-04T14:50:37.650+02:00</created_date>
+		<modified_date>2020-10-13T04:41:39.757+02:00</modified_date>
+		<deleted_date></deleted_date>
+		<services>
+			<battery_meter id='4ba77a1d403348948b36b73e0b850e9a' log_type='battery'>
+				<functionalities><point_log id='64842f9da6d44b699ba14d015b042e6f'/></functionalities>
+			</battery_meter>
+			<thermostat id='4dadc7a116c94b489c2d94d404ad5afb' log_type='thermostat'>
+				<functionalities><thermostat_functionality id='07817e0fe40446689f2d136a23f33b23'/><point_log id='fa54e2be97804658920f0694bb1e0b10'/></functionalities>
+			</thermostat>
+			<thermo_meter id='a46ea40c6c9a4548b8093f99d844e35a' log_type='uncorrected_temperature'>
+				<functionalities><point_log id='3d78399888fb4e16939d8943b2b2a21a'/></functionalities>
+			</thermo_meter>
+			<thermo_meter id='a58d919de1694058ae9817aa9d67cb47' log_type='temperature_difference'>
+				<functionalities><point_log id='aef3913b6e0649bcb8098c77c004fc53'/></functionalities>
+			</thermo_meter>
+			<valve_position_meter id='d8be21dd31f94eb4bdc937d963d089d3' log_type='valve_position'>
+				<functionalities><point_log id='b5b280cbda6c4bf9840e4f125e1affe1'/></functionalities>
+			</valve_position_meter>
+			<temperature_offset id='edc13ab75438426a95b2fe013707be54' log_type='temperature_offset'>
+				<functionalities><point_log id='1c1a75498bc54efaa8c544d376fd67c8'/><offset_functionality id='c282082ba56e49ae888131587fee8d28'/></functionalities>
+			</temperature_offset>
+			<thermo_meter id='f561a48b6f644dc384ba7cf0cafe8184' log_type='temperature'>
+				<functionalities><point_log id='970e8f1902ff45cfba96ffd4654fc3f1'/></functionalities>
+			</thermo_meter>
+		</services>
+		<protocols>
+			<zig_bee_node id='9074cd5161764e8eb0261d5ba99a0428'>
+				<mac_address>0123456789AB</mac_address>
+				<type>end_device</type>
+				<reachable>true</reachable>
+				<power_source>battery</power_source>
+				<override_antenna_power>false</override_antenna_power>
+				<blocked_current>0</blocked_current>
+				<zig_bee_coordinator id='fcd3478dede94c5c87b5ea39a3404708'/>
+				<neighbors><zig_bee_node id='87f66dac0b92499897598c95fe55286e'/><zig_bee_coordinator id='fcd3478dede94c5c87b5ea39a3404708'/></neighbors>
+			</zig_bee_node>
+		</protocols>
+	</module>
+	<module id='0fd8411d0bc446a08c336a7bba029e22'>
+		<vendor_name>Plugwise</vendor_name>
+		<vendor_model>160-01</vendor_model>
+		<hardware_version></hardware_version>
+		<firmware_version>2019-06-21T02:00:00+02:00</firmware_version>
+		<upgrade/>
+		<created_date>2020-02-28T17:37:35.085+01:00</created_date>
+		<modified_date>2020-02-28T17:47:49.324+01:00</modified_date>
+		<deleted_date></deleted_date>
+		<services>
+			<electricity_point_meter id='f155298962a14d928ac25651a71384b8' log_type='electricity'>
+				<functionalities><point_log id='3c95dcb1682a416e9aeb363bf63cd952'/><point_log id='432abf2d33264846b7107e31ed4d9ee5'/></functionalities>
+			</electricity_point_meter>
+			<relay id='f1f2a33722014b46882f310a760a6964' log_type='relay'>
+				<functionalities><point_log id='8eb6b3c5e5f34b908419e5e10a778881'/><relay_functionality id='cf593f271bd447cea60b8644410092c8'/></functionalities>
+			</relay>
+			<electricity_interval_meter id='ffeae7e3e0b74e339080ff73e8eb448d' log_type='electricity'>
+				<functionalities><interval_log id='a0652ca1118749849704a5e26bdf8632'/><interval_log id='c03901aabb32489bbba580e13bc00186'/></functionalities>
+			</electricity_interval_meter>
+		</services>
+		<protocols>
+			<zig_bee_node id='9ca5df0038564aeeb59709ab3de23f97'>
+				<mac_address>0123456789AB</mac_address>
+				<type>router</type>
+				<reachable>true</reachable>
+				<power_source>mains</power_source>
+				<zig_bee_coordinator id='fcd3478dede94c5c87b5ea39a3404708'/>
+				<neighbors><zig_bee_node id='a7c4a564cd4943a1ab9eee428270b2ae'/><zig_bee_coordinator id='fcd3478dede94c5c87b5ea39a3404708'/></neighbors>
+			</zig_bee_node>
+		</protocols>
+	</module>
+	<module id='72b2aa43d2ea4a33b23ade5e6629de8d'>
+		<vendor_name>Plugwise</vendor_name>
+		<vendor_model>160-01</vendor_model>
+		<hardware_version></hardware_version>
+		<firmware_version>2019-06-21T02:00:00+02:00</firmware_version>
+		<upgrade/>
+		<created_date>2020-02-29T22:56:58.823+01:00</created_date>
+		<modified_date>2020-02-29T23:05:15.393+01:00</modified_date>
+		<deleted_date></deleted_date>
+		<services>
+			<electricity_point_meter id='331ec93d127549a497da984d333aecf0' log_type='electricity'>
+				<functionalities><point_log id='961561ef356745a9afae11101d383009'/><point_log id='c2b79da7a61745d589aa473bf38c5e54'/></functionalities>
+			</electricity_point_meter>
+			<electricity_interval_meter id='5239020854b547318dca68fe4d287ab1' log_type='electricity'>
+				<functionalities><interval_log id='1077f68c1416449fb73db5037d002467'/><interval_log id='bb891a2580d44a8aa8df78692031c7c3'/></functionalities>
+			</electricity_interval_meter>
+			<relay id='9f35a3b3d6094816952e801f66964849' log_type='relay'>
+				<functionalities><point_log id='c4f632f6ff6546dba40bad5c7249bbc3'/><relay_functionality id='dc6998c6abcb4fec94db2043f78a29c1'/></functionalities>
+			</relay>
+		</services>
+		<protocols>
+			<zig_bee_node id='87f66dac0b92499897598c95fe55286e'>
+				<mac_address>0123456789AB</mac_address>
+				<type>router</type>
+				<reachable>false</reachable>
+				<power_source>mains</power_source>
+				<zig_bee_coordinator id='fcd3478dede94c5c87b5ea39a3404708'/>
+				<neighbors><zig_bee_node id='9074cd5161764e8eb0261d5ba99a0428'/></neighbors>
+			</zig_bee_node>
+		</protocols>
+	</module>
+	<module id='e103f210bcf74569814d9d9c8725d6a2'>
+		<vendor_name>Plugwise</vendor_name>
+		<vendor_model>158-01</vendor_model>
+		<hardware_version>255</hardware_version>
+		<firmware_version>2016-10-10T02:00:00+02:00</firmware_version>
+		<upgrade/>
+		<created_date>2020-04-04T15:00:00.280+02:00</created_date>
+		<modified_date>2020-04-04T15:01:39.322+02:00</modified_date>
+		<deleted_date></deleted_date>
+		<services>
+			<battery_meter id='1eb270d61d2b4aa0952d56818cd67d50' log_type='battery'>
+				<functionalities><point_log id='55252d9afe6847f0afb8448c1130b9ca'/></functionalities>
+			</battery_meter>
+			<thermostat id='63a8720ba6954ed8b8268ae571de9294' log_type='thermostat'>
+				<functionalities><thermostat_functionality id='2d38953189384f2c902d9b1f1f319f4b'/><point_log id='a8ab01fb5fa943ec909e1a4baf83d197'/></functionalities>
+			</thermostat>
+			<thermo_meter id='8e3c6be20e034ad79e31fbe842a65078' log_type='temperature'>
+				<functionalities><point_log id='fc2f48f9071647d19b4f5564d1b5d65d'/></functionalities>
+			</thermo_meter>
+			<temperature_offset id='e6d5fb9dff564ef3a90e8374f33c24bf' log_type='temperature_offset'>
+				<functionalities><offset_functionality id='3f17502fe6cb4557ac6901b9eb8f5939'/><point_log id='e06e768c5b734bb48916e1470da69ddd'/></functionalities>
+			</temperature_offset>
+		</services>
+		<protocols>
+			<zig_bee_node id='0374084c2e2843f19688e289235859ce'>
+				<mac_address>0123456789AB</mac_address>
+				<type>end_device</type>
+				<reachable>true</reachable>
+				<power_source>battery</power_source>
+				<zig_bee_coordinator id='fcd3478dede94c5c87b5ea39a3404708'/>
+				<neighbors><zig_bee_coordinator id='fcd3478dede94c5c87b5ea39a3404708'/></neighbors>
+			</zig_bee_node>
+		</protocols>
+	</module>
+	<module id='5712c009d4c24e3c8be5beedbc6ccc16'>
+		<vendor_name>Plugwise</vendor_name>
+		<vendor_model>Adam</vendor_model>
+		<hardware_version>6539-1601-4502</hardware_version>
+		<firmware_version>2018-10-30T10:12:41+01:00</firmware_version>
+		<created_date>2019-08-15T16:18:38.925+02:00</created_date>
+		<modified_date>2020-02-22T15:38:53.823+01:00</modified_date>
+		<deleted_date></deleted_date>
+		<services>
+			<power_mode id='89adef467daf40b1a117062ac1ceecb8' log_type='open_therm_power_mode'>
+				<functionalities><point_log id='8f9c88aba5db44d5a3d6480421355055'/></functionalities>
+			</power_mode>
+			<override_mode id='d425110eae6b4ac4bb4d9bcebd33fca6' log_type='override_mode'>
+				<functionalities><point_log id='62f9f218a8f24016945e1d33ba986a06'/></functionalities>
+			</override_mode>
+		</services>
+		<protocols>
+			<open_therm_gateway id='81dacf5db3b94bfd9bdeef364fc07234'>
+				<serial>16-50-001-505</serial>
+				<open_therm_thermostat id='a56dbe7454c543f7a473c4e4d9a9f30f'/>
+				<open_therm_boiler id='c4812cb960f94e808eeb1cf9be566e24'/>
+			</open_therm_gateway>
+		</protocols>
+	</module>
+	<module id='46ee18c6f9a442739aacd5254ba3a9c2'>
+		<vendor_name>Plugwise</vendor_name>
+		<vendor_model>160-01</vendor_model>
+		<hardware_version></hardware_version>
+		<firmware_version>2019-06-21T02:00:00+02:00</firmware_version>
+		<upgrade/>
+		<created_date>2020-04-06T19:13:29.360+02:00</created_date>
+		<modified_date>2020-04-06T19:22:01.431+02:00</modified_date>
+		<deleted_date></deleted_date>
+		<services>
+			<electricity_point_meter id='0df0f98926cf4673afcd7f968ddc49aa' log_type='electricity'>
+				<functionalities><point_log id='d8348e1ee73544da9863d3022d6a577e'/><point_log id='dea0c49f659f462ab10ff4942a6a810e'/></functionalities>
+			</electricity_point_meter>
+			<electricity_interval_meter id='3fe449323e6749dfa29e9933e227199c' log_type='electricity'>
+				<functionalities><interval_log id='87c2adcee4a2477a8ce6f876e3f02132'/><interval_log id='99c2693e9d944460b95b506808639e78'/></functionalities>
+			</electricity_interval_meter>
+			<relay id='901ffc0372e34d889041de81b2097f38' log_type='relay'>
+				<functionalities><point_log id='a0bab1094a1c404187c4b683d905aa48'/><relay_functionality id='f03fda2f2f494c198701a4cdea790ab9'/></functionalities>
+			</relay>
+		</services>
+		<protocols>
+			<zig_bee_node id='a7c4a564cd4943a1ab9eee428270b2ae'>
+				<mac_address>0123456789AB</mac_address>
+				<type>router</type>
+				<reachable>true</reachable>
+				<power_source>mains</power_source>
+				<zig_bee_coordinator id='fcd3478dede94c5c87b5ea39a3404708'/>
+				<neighbors><zig_bee_node id='9ca5df0038564aeeb59709ab3de23f97'/><zig_bee_coordinator id='fcd3478dede94c5c87b5ea39a3404708'/></neighbors>
+			</zig_bee_node>
+		</protocols>
+	</module>
+	<module id='94c63e8b46284f6ab2f40d19619afcba'>
+		<vendor_name></vendor_name>
+		<vendor_model></vendor_model>
+		<hardware_version></hardware_version>
+		<firmware_version></firmware_version>
+		<created_date>2020-02-22T15:18:04.283+01:00</created_date>
+		<modified_date>2020-10-13T04:29:59.289+02:00</modified_date>
+		<deleted_date></deleted_date>
+		<services>
+			<boiler_state id='051a403e28d34db7ac05a8440eec380a' log_type='flame_state'>
+				<functionalities><point_log id='9c7be20add794278991b7d92daef0465'/></functionalities>
+			</boiler_state>
+			<thermostat id='06e7838827b845ccac752b06878263f8' log_type='domestic_hot_water_setpoint'>
+				<functionalities><thermostat_functionality id='367a2fa93ae54faaab3cfcee33070e31'/><point_log id='84b35d9ba6a2452ca297ef13e14ebe20'/></functionalities>
+			</thermostat>
+			<thermo_meter id='3c424cf18f3949eaa52bd38ca098ac12' log_type='intended_boiler_temperature'>
+				<functionalities><point_log id='1e502f880a614e24abf08728b54524ce'/></functionalities>
+			</thermo_meter>
+			<boiler_state id='3e69a63e70d84ba0b2cc8feb523275fd' log_type='intended_central_heating_state'>
+				<functionalities><point_log id='41abcb8e89354be4bbc6b42a29422af8'/></functionalities>
+			</boiler_state>
+			<fault_code id='3edbe9ff5d074e999df4481bbda9eacb' log_type='open_therm_oem_fault_code'>
+				<functionalities><point_log id='d1dc0324b1a74436aa79deffa59791a7'/></functionalities>
+			</fault_code>
+			<cumulative_counter id='50c63cd482a74e8b9be25535c806d722' log_type='failed_burner_flame_ignitions'>
+				<functionalities><cumulative_log id='a4dd30d53af5426a92b0af1eacf24af1'/></functionalities>
+			</cumulative_counter>
+			<thermostat id='7b7ca11faa024f01994dc68c4029db66' log_type='maximum_boiler_temperature'>
+				<functionalities><thermostat_functionality id='997e18f536fa400087e515e1bde37611'/><point_log id='f990794a43b441e2a9db48f15aa88307'/></functionalities>
+			</thermostat>
+			<cumulative_chrono_meter id='b1e6249642f44459a5bb7d1bbc115241' log_type='domestic_hot_water_burner_operation_time'>
+				<functionalities><cumulative_log id='fe985bbd4f9d48979b747e0078467421'/></functionalities>
+			</cumulative_chrono_meter>
+			<cumulative_counter id='b3baea986fe74e4d8d9b63fbfd6ae8ff' log_type='failed_burner_starts'>
+				<functionalities><cumulative_log id='ef060f0d9673464db63cd3c6e7239854'/></functionalities>
+			</cumulative_counter>
+			<interval_chrono_meter id='b63b88f3ff3f4abea124c230c16a1f09' log_type='burner_operation_time'>
+				<functionalities><interval_log id='b088d9b5cbe243f5a6f784eec3f83868'/></functionalities>
+			</interval_chrono_meter>
+			<boiler_state id='c38d5ff7ff8f46ce8e14be36765c62c9' log_type='central_heating_state'>
+				<functionalities><point_log id='7c0d55cfd2b24552bbc8a5b3facd5882'/></functionalities>
+			</boiler_state>
+			<fault_code id='ceecf49c519b4dbc857822e8275be6d5' log_type='open_therm_application_specific_fault_code'>
+				<functionalities><point_log id='ace6c3ff382c43cdaac964b73b24763f'/></functionalities>
+			</fault_code>
+			<boiler_state id='d4b05bd552b44574933f6bc2717fcbe9' log_type='domestic_hot_water_state'>
+				<functionalities><point_log id='26d0cc991be8420f98d043e7c7254954'/></functionalities>
+			</boiler_state>
+			<domestic_hot_water_toggle id='d4e7924100d44d62b96e65f10ba4f4d6' log_type='domestic_hot_water_comfort_mode'>
+				<functionalities><toggle_functionality id='63c3c13ba02f414cbb3d19a2a47bd025'/><point_log id='e3389c255fe440b399e2ae3dc197ed29'/></functionalities>
+			</domestic_hot_water_toggle>
+			<cumulative_chrono_meter id='dd4b8f45465645d3a58a88411f719f12' log_type='burner_operation_time'>
+				<functionalities><cumulative_log id='084a643b2a134f5bb7afc4ba09fcf44d'/></functionalities>
+			</cumulative_chrono_meter>
+			<boiler_state id='dd7213afa98a4d00bcc7f5e8a46dbdee' log_type='cooling_state'>
+				<functionalities><point_log id='1ef800514a384f04a3317da47736be43'/></functionalities>
+			</boiler_state>
+			<pressure_gauge id='e6a14cbac0674106bb539f159dafff53' log_type='central_heater_water_pressure'>
+				<functionalities><point_log id='a56ef979c3be45f7b6e93411c285e342'/></functionalities>
+			</pressure_gauge>
+			<thermo_meter id='ef3adfb4915e4763b22d1d13611bec4f' log_type='temperature'>
+				<functionalities><point_log id='f237d09f259b41859e055fa4ccdf4867'/></functionalities>
+			</thermo_meter>
+			<thermo_meter id='f2542b0f32fb4816bb6e7bdc7378e802' log_type='boiler_temperature'>
+				<functionalities><point_log id='ec490bd52b334ac6946f157a81eebe50'/></functionalities>
+			</thermo_meter>
+			<cumulative_counter id='f71e1318981c4a6d9fb1cb46212c1178' log_type='burner_starts'>
+				<functionalities><cumulative_log id='0ff776327d864e0ca70c9f6e7ba02048'/></functionalities>
+			</cumulative_counter>
+		</services>
+		<protocols>
+			<open_therm_boiler id='c4812cb960f94e808eeb1cf9be566e24'>
+				<open_therm_gateway id='81dacf5db3b94bfd9bdeef364fc07234'/>
+				<open_therm_version>0.0</open_therm_version>
+				<smartgrid_control>smart_grid_off</smartgrid_control>
+				<test_mode>off</test_mode>
+			</open_therm_boiler>
+		</protocols>
+	</module>
+	<module id='596a05d3775f408ebab7d28a90a4e865'>
+		<vendor_name>Plugwise</vendor_name>
+		<vendor_model>Gateway</vendor_model>
+		<hardware_version>AME Smile 2.0 board</hardware_version>
+		<firmware_version></firmware_version>
+		<created_date>2019-08-15T16:18:29.628+02:00</created_date>
+		<modified_date>2020-10-13T04:29:35.355+02:00</modified_date>
+		<deleted_date></deleted_date>
+		<services>
+			<network_state id='218e8016d137488eaa66974c06001877' log_type='wlan_state'>
+				<functionalities><point_log id='17cb5c2e06864fd2a013d8e7431f277a'/></functionalities>
+			</network_state>
+			<network_state id='2a2749bc5b064beaa5d4b8e73d030b5c' log_type='lan_state'>
+				<functionalities><point_log id='e089d6079bb04048a65bf5cf3a8919e9'/></functionalities>
+			</network_state>
+			<network_address id='863b10fc324c41e5a6d56890c7a79048' log_type='wlan_ip_address'>
+				<functionalities><point_log id='59bfe7a344d6473ab37eb097be06875b'/></functionalities>
+			</network_address>
+			<regulation_mode_control id='8d5cc0518b9e4425a4e8bcb3bcdd3abc' log_type='regulation_mode'>
+				<functionalities><regulation_mode_control_functionality id='adbef84d1fd849898bd190e1832e4331'/><point_log id='aebd1598cb3c48bdaa75af147e03f5ca'/></functionalities>
+			</regulation_mode_control>
+			<signal_strength id='c33a60e93d2f4aa3be647ae0406447a1' log_type='signal_strength'>
+				<functionalities><point_log id='dfdc09f8ffa047ddb79212a717e6309f'/></functionalities>
+			</signal_strength>
+			<network_address id='cbf5fd20c1124b7a9e079709d5f7c2c2' log_type='lan_ip_address'>
+				<functionalities><point_log id='e115e5cd70a74b4f8a9a07400b98941e'/></functionalities>
+			</network_address>
+			<link_quality id='e0c47bd61e9949fd8bd48dd5cbdc0c83' log_type='link_quality'>
+				<functionalities><point_log id='c085d1b726614aeeb1fc7b0ea7d6a33b'/></functionalities>
+			</link_quality>
+		</services>
+		<protocols>
+			<local_area_network id='809559d4a70a45a4a1ae052732bf737b'/>
+			<wireless_local_area_network id='8a6c82a985f64b5aa50ffa4fb19e6ba2'/>
+		</protocols>
+	</module>
+	<module id='152f171629394a349a4813450555c62c'>
+		<vendor_name></vendor_name>
+		<vendor_model></vendor_model>
+		<hardware_version></hardware_version>
+		<firmware_version></firmware_version>
+		<created_date>2019-08-15T16:18:30.152+02:00</created_date>
+		<modified_date>2020-02-22T15:18:11.647+01:00</modified_date>
+		<deleted_date></deleted_date>
+		<services>
+			<thermo_meter id='69f3d8c8a20b427e9ba4cbed41bd0119' log_type='outdoor_temperature'>
+				<functionalities><point_log id='5fa52667df6f4f27a6d946a88b96e26a'/></functionalities>
+			</thermo_meter>
+			<weather_descriptor id='85a71638d58644798b7de745fc6e7fba' log_type='weather_description'>
+				<functionalities><point_log id='9092e027a62c4940b1e6720c2931d594'/></functionalities>
+			</weather_descriptor>
+			<wind_vector id='8d575a00343548bdb5e3bfe53d3dcfc5' log_type='wind_vector'>
+				<functionalities><point_log id='c9ccffeb098349cdabe92107e44d20d7'/></functionalities>
+			</wind_vector>
+			<irradiance_meter id='8d8863f2e7bf468db0c0cdba424a7963' log_type='solar_irradiance'>
+				<functionalities><point_log id='5bf073ace78840d49192a547b7dc74b6'/></functionalities>
+			</irradiance_meter>
+			<hygro_meter id='e441bf895e214ca9a660b37bd41bfe63' log_type='humidity'>
+				<functionalities><point_log id='4d0a474a9ac24c41a7d38ed05556b125'/></functionalities>
+			</hygro_meter>
+		</services>
+		<protocols>
+			<weather_feed id='82d9d306b84745d2af24be76ce5fb8f2'/>
+		</protocols>
+	</module>
+</modules>
diff --git a/userdata/adam_zone_per_device/core.appliances.xml b/userdata/adam_zone_per_device/core.appliances.xml
index da22af946..5f1628789 100644
--- a/userdata/adam_zone_per_device/core.appliances.xml
+++ b/userdata/adam_zone_per_device/core.appliances.xml
@@ -86,7 +86,7 @@
 				<unit></unit>
 				<last_consecutive_log_date>2020-03-20T12:15:29.245+01:00</last_consecutive_log_date>
 				<period start_date="2020-03-20T12:15:29.245+01:00" end_date="2020-03-20T12:15:29.245+01:00">
-					<measurement log_date="2020-03-20T12:15:29.245+01:00">1.00</measurement>
+					<measurement log_date="2020-03-20T12:15:29.245+01:00">0.00</measurement>
 				</period>
 				<valve_position_meter id='1db7d08fabb24bdb8f5fdfd5d317cac5'/>
 			</point_log>
diff --git a/userdata/anna_v4_no_tag/core.modules.xml b/userdata/anna_v4_no_tag/core.modules.xml
new file mode 100644
index 000000000..5606564f3
--- /dev/null
+++ b/userdata/anna_v4_no_tag/core.modules.xml
@@ -0,0 +1,243 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<modules>
+	<module id='a6ce9051e75a41d89224c0de5209b6f4'>
+		<vendor_name></vendor_name>
+		<vendor_model></vendor_model>
+		<hardware_version></hardware_version>
+		<firmware_version></firmware_version>
+		<created_date>2019-01-31T10:06:29.692+01:00</created_date>
+		<modified_date>2020-04-01T05:19:49.019+02:00</modified_date>
+		<deleted_date></deleted_date>
+		<services>
+			<wind_vector id='14c1bfef965f401eb5a82f9831eb8b6b' log_type='wind_vector'>
+				<functionalities><point_log id='a2970830f8e8447ba6b12dbe8e506193'/></functionalities>
+			</wind_vector>
+			<hygro_meter id='28e1e7ec19eb4f89a21f1a25acf737c7' log_type='humidity'>
+				<functionalities><point_log id='1f3e3a4729fb4339a0e8dcea0b41a71f'/></functionalities>
+			</hygro_meter>
+			<weather_descriptor id='2cee5b91d17e40e38acfbb93988a5b5e' log_type='weather_description'>
+				<functionalities><point_log id='4b547120a3854d7e8d00ef6bc1d8703c'/></functionalities>
+			</weather_descriptor>
+			<thermo_meter id='44e85a52350b4ff2befe01f4856d7d9e' log_type='outdoor_temperature'>
+				<functionalities><point_log id='4316e58b707a4526a88e54b64b3d5cac'/></functionalities>
+			</thermo_meter>
+			<irradiance_meter id='cc06ea07c5194981b5963f949f5f2086' log_type='solar_irradiance'>
+				<functionalities><point_log id='af9337f554e34909884aa48fe1770c52'/></functionalities>
+			</irradiance_meter>
+		</services>
+		<protocols>
+			<weather_feed id='7abec3e15ad64057b751db2cdf112919'/>
+		</protocols>
+	</module>
+	<module id='71c43d352c64465da863adc524891502'>
+		<vendor_name>Plugwise</vendor_name>
+		<vendor_model>ThermoExtension</vendor_model>
+		<hardware_version>6539-1301-2304</hardware_version>
+		<firmware_version>2018-02-08T11:15:57+01:00</firmware_version>
+		<created_date>2019-01-31T10:06:34.781+01:00</created_date>
+		<modified_date>2019-05-14T20:03:36.442+02:00</modified_date>
+		<deleted_date></deleted_date>
+		<services/>
+		<protocols>
+			<thermo_extension id='d8ade33622cf4f72a862dfde7ac32e8b'><thermo_touch id='a82b0c5dc4094286a10c6f8f231d2145'/><open_therm_boiler id='d0c69c8c5f674adda8a499b7737feaaf'/></thermo_extension>
+		</protocols>
+	</module>
+	<module id='dd04f9f062fc46ccb200296740fbc19b'>
+		<vendor_name>Plugwise</vendor_name>
+		<vendor_model>Gateway</vendor_model>
+		<hardware_version>AME Smile 2.0 board</hardware_version>
+		<firmware_version></firmware_version>
+		<created_date>2019-01-31T10:06:29.380+01:00</created_date>
+		<modified_date>2019-01-31T10:11:04.174+01:00</modified_date>
+		<deleted_date></deleted_date>
+		<services>
+			<signal_strength id='2c2f3c1e3b69422ca8bcc48c4b8418a6' log_type='signal_strength'>
+				<functionalities><point_log id='7b6f4d5cf9b44948a078bfd6cc0c232c'/></functionalities>
+			</signal_strength>
+			<link_quality id='bb5b16e8b65f414e92d3f6078c9e7131' log_type='link_quality'>
+				<functionalities><point_log id='70f567f5bed34cae890933e927c39e8d'/></functionalities>
+			</link_quality>
+			<network_address id='bd195e84b64849a698bbdaa51971d5bf' log_type='wlan_ip_address'>
+				<functionalities><point_log id='3ebe31f8526348a2840e2d26cbd044f8'/></functionalities>
+			</network_address>
+			<network_state id='d973960ae3704febb038a7440dc183ff' log_type='wlan_state'>
+				<functionalities><point_log id='63dd70d1f09b47a49a0a89e3aa5a0ac1'/></functionalities>
+			</network_state>
+			<network_state id='e0f1c5dc487045e69a264d76930cb032' log_type='lan_state'>
+				<functionalities><point_log id='f7c5f09e3711420e835dcc9b75beb19c'/></functionalities>
+			</network_state>
+		</services>
+		<protocols>
+			<wireless_local_area_network id='6d372674734e4f09a53b846757e1c5e6'/>
+			<local_area_network id='a3e6fdaa921b47b2b4c4fc9bd039530c'/>
+		</protocols>
+	</module>
+	<module id='807a45cbc950419c98ce4624bc27ff09'>
+		<vendor_name>Plugwise</vendor_name>
+		<vendor_model>ThermoTouch</vendor_model>
+		<hardware_version>6539-1301-5002</hardware_version>
+		<firmware_version>2018-02-08T11:15:53+01:00</firmware_version>
+		<created_date>2019-01-31T10:06:52.148+01:00</created_date>
+		<modified_date>2020-04-01T05:19:35.003+02:00</modified_date>
+		<deleted_date></deleted_date>
+		<services>
+			<proximity_sensor id='0223582f325a4af0af1473b20286901a' log_type='proximity_detection'>
+				<functionalities><point_log id='12368fa0f4d94c1c9476cd2e01476ef4'/></functionalities>
+			</proximity_sensor>
+			<illuminance_meter id='049286decbe142e6bbeea571a2333e68' log_type='illuminance'>
+				<functionalities><point_log id='accc9852d115482b86715d5400eaec5b'/></functionalities>
+			</illuminance_meter>
+			<temperature_offset_meter id='11aec25914614c6d8a1eb5f90e89884c' log_type='onoff_boiler_temperature_deadband'>
+				<functionalities><point_log id='50675e01fb144333a48cc0193c8d2b2a'/></functionalities>
+			</temperature_offset_meter>
+			<thermostat id='11fbf4c670974101868a1239d2d4a5db' log_type='thermostat'>
+				<functionalities><thermostat_functionality id='83af7792c5424a629c0003fccd110e27'/><point_log id='c0d63ecde8d04ed7ae92dd950f27b31b'/></functionalities>
+			</thermostat>
+			<boiler_state id='15063ed7cdc04e1b9c7341d4090ccdbb' log_type='intended_central_heating_state'>
+				<functionalities><point_log id='c0b4161b26e84198ac4b95a973be940e'/></functionalities>
+			</boiler_state>
+			<frequency_meter id='3f45ee1c9c62415e876ad445f836e012' log_type='cpu_frequency'>
+				<functionalities><point_log id='9c07c88a1332489692533e96e0a4692c'/></functionalities>
+			</frequency_meter>
+			<temperature_offset id='40e47835f2c9440bb5045e8497b967b9' log_type='temperature_offset'>
+				<functionalities><offset_functionality id='0b18f5185a18438d85fa394e7d568594'/><point_log id='ace7da3840f7466eacbcfe8138f4cc6d'/></functionalities>
+			</temperature_offset>
+			<coefficient id='65a34d92edc64077943114901a711466' log_type='derivative_coefficient'>
+				<functionalities><point_log id='430ae0b8da2e409596ab117fa4003bf6'/></functionalities>
+			</coefficient>
+			<temperature_offset_meter id='65c6a99f3d644d9e9e9bb6609291f642' log_type='open_therm_boiler_activation_threshold'>
+				<functionalities><point_log id='26283600fedb484895b1dbf3402ef995'/></functionalities>
+			</temperature_offset_meter>
+			<proximity_sensor_toggle id='701fc1b8c8e0449685a24e9352a54b7a' log_type='proximity_sensor_state'>
+				<functionalities><point_log id='b0c7267101a14627bd435ba486f3b00c'/><toggle_functionality id='f898f899041745ca919e1c290d6760ed'/></functionalities>
+			</proximity_sensor_toggle>
+			<schedule_state id='7d9f4a1cf4434dd2962d502776fc640e' log_type='schedule_state'>
+				<functionalities><point_log id='ba145db35ac34d0d9c7e5f95d60999cf'/></functionalities>
+			</schedule_state>
+			<button id='9075259bfafc4f8cb584bf47eed69159' log_type='button_left' position='left'>
+				<functionalities><point_log id='d2af54fa8fc34a67bccf4faa5867a466'/></functionalities>
+			</button>
+			<thermo_meter id='97979ed55a8548a18cac1999981fc103' log_type='temperature'>
+				<functionalities><point_log id='3ea1912e69014018a7fe04fda9fff58c'/></functionalities>
+			</thermo_meter>
+			<preset_state id='9bd7651dbb034968aa3445260fc17a4a' log_type='preset_state'>
+				<functionalities><point_log id='58898de1009849d0bd4ea6ec2c540fb8'/></functionalities>
+			</preset_state>
+			<boiler_state id='c0d93bfb5f244065ac4e121187b4a9dd' log_type='intended_domestic_hot_water_comfort_mode'>
+				<functionalities><point_log id='e36fccf7e7354d6293027499d997c52c'/></functionalities>
+			</boiler_state>
+			<button id='d47c1fdbb658408085ba19dd67d3e642' log_type='button_top' position='top'>
+				<functionalities><point_log id='bf0b2df8d4194994af086256b2e22c9c'/></functionalities>
+			</button>
+			<button id='e38a2dbd6c184a42b3c08409bb8760b9' log_type='button_right' position='right'>
+				<functionalities><point_log id='9ca50f5427194fefacdad3bbc17f9492'/></functionalities>
+			</button>
+			<temperature_offset_meter id='efe20d4bf1044ecc83e437d8553a834a' log_type='decentral_heat_demand_offset'>
+				<functionalities><point_log id='233716f6b93f4c0ca741c70fd3a7b917'/></functionalities>
+			</temperature_offset_meter>
+			<temperature_rate id='f4e9de649aa6456999874483fa991af7' log_type='preheating_target_temperature_rate'>
+				<functionalities><point_log id='ccacb04b3e0b4beabba4c962942e62d9'/></functionalities>
+			</temperature_rate>
+			<thermo_meter id='f92e9b1bee8543368faea6a96f05c389' log_type='target_temperature'>
+				<functionalities><point_log id='f37d07768c97454c8264ea70408921ae'/></functionalities>
+			</thermo_meter>
+		</services>
+		<protocols>
+			<thermo_touch id='a82b0c5dc4094286a10c6f8f231d2145'><thermo_extension id='d8ade33622cf4f72a862dfde7ac32e8b'/></thermo_touch>
+		</protocols>
+	</module>
+	<module id='88c1299c194f4b39b857e369803c2481'>
+		<vendor_name>Bosch Thermotechniek B.V.</vendor_name>
+		<vendor_model>2.32</vendor_model>
+		<hardware_version></hardware_version>
+		<firmware_version></firmware_version>
+		<created_date>2019-05-14T20:03:36.453+02:00</created_date>
+		<modified_date>2020-04-01T05:20:02.787+02:00</modified_date>
+		<deleted_date></deleted_date>
+		<services>
+			<interval_chrono_meter id='0a3d66763cdc49d593d63bd41f4a711f' log_type='burner_operation_time'>
+				<functionalities><interval_log id='34eb0c52029b49158cea1bbebf179daf'/></functionalities>
+			</interval_chrono_meter>
+			<flow_meter id='0ceafe98a0da45c9b15da4be67a68be4' log_type='domestic_hot_water_flow'>
+				<functionalities><point_log id='146cc6e679c04f03aa943dcf060cbc49'/></functionalities>
+			</flow_meter>
+			<point_chrono_meter id='0e60b41f8bfd472eb48b9444b8830fec' log_type='burner_efficiency'>
+				<functionalities><point_log id='c8848b0b589a4cd3b48ee6386637a8fe'/></functionalities>
+			</point_chrono_meter>
+			<boiler_state id='0e6517c9fed14e54867211caef4ba408' log_type='flame_state'>
+				<functionalities><point_log id='14dc3708bf3643c6b4fc50d0f17f0634'/></functionalities>
+			</boiler_state>
+			<thermo_meter id='1665a44e985b4cddbf4857acc148c5c2' log_type='intended_boiler_temperature'>
+				<functionalities><point_log id='2a57990fc3de4f4a99b27c3d9aa29991'/></functionalities>
+			</thermo_meter>
+			<fault_code id='1ef0e90efc344271a170f39a74b35ce4' log_type='open_therm_application_specific_fault_code'>
+				<functionalities><point_log id='466395edd8c046e7bb5099f5453f6827'/></functionalities>
+			</fault_code>
+			<pressure_gauge id='249ebc6c93c5478f8fc8b2263ffb5442' log_type='central_heater_water_pressure'>
+				<functionalities><point_log id='fae77136ea584535b5e775bea84c4b56'/></functionalities>
+			</pressure_gauge>
+			<thermo_meter id='3b4c70cd232a4f98b982b1333c551b03' log_type='boiler_temperature'>
+				<functionalities><point_log id='247e468e80ab4ee7a265f30d6f7351a0'/></functionalities>
+			</thermo_meter>
+			<thermostat id='43f1d74528304726b9b08c2d31faf25d' log_type='domestic_hot_water_setpoint'>
+				<functionalities><point_log id='441d06bceaaa4b4793080345b7340145'/><thermostat_functionality id='5be289822084461d8e0416df48cab4e4'/></functionalities>
+			</thermostat>
+			<cumulative_chrono_meter id='4f78a43506574466b715819132dc72d3' log_type='burner_operation_time'>
+				<functionalities><cumulative_log id='c363228a299f48c4bf611e26443fea4b'/></functionalities>
+			</cumulative_chrono_meter>
+			<interval_chrono_meter id='65927026812340eaba23d17e6f075e1a' log_type='burner_efficiency'>
+				<functionalities><interval_log id='8dfd1f785ef64e549b09fb4a0d215aaa'/></functionalities>
+			</interval_chrono_meter>
+			<status_code id='7e250ee57aa746759be40804e76f691e' log_type='open_therm_oem_diagnostic_code'>
+				<functionalities><point_log id='d005ee605f0f45a88e01ff86dec9d580'/></functionalities>
+			</status_code>
+			<boiler_state id='8bd0723823b14ce69ca622519f9434a6' log_type='domestic_hot_water_state'>
+				<functionalities><point_log id='7213fa03e46e4960942066936bbf589d'/></functionalities>
+			</boiler_state>
+			<domestic_hot_water_toggle id='a9a769caed18433387e4df51b54727f4' log_type='domestic_hot_water_comfort_mode'>
+				<functionalities><point_log id='2c486dfa1787496e8deccf8e75ccb97b'/><toggle_functionality id='7f272221395344efa47aed40f4da58c9'/></functionalities>
+			</domestic_hot_water_toggle>
+			<cumulative_counter id='aed6163894cc49a3b41ab1c6d860d59a' log_type='domestic_hot_water_burner_starts'>
+				<functionalities><cumulative_log id='3969e30b0d3b45d2ae41bd3b76f72af3'/></functionalities>
+			</cumulative_counter>
+			<cumulative_counter id='b5335c2d1bb543299aeea76396d5fa92' log_type='burner_starts'>
+				<functionalities><cumulative_log id='c1f902546b9e43fca1082b76971e164b'/></functionalities>
+			</cumulative_counter>
+			<thermostat id='c4c89f5b53f1432a948fddb3e82821e8' log_type='maximum_boiler_temperature'>
+				<functionalities><point_log id='74eb7fd34ab24433a0325286d45e6bf4'/><thermostat_functionality id='b8b6121886d04f41bdb62585844d3b0e'/></functionalities>
+			</thermostat>
+			<boiler_state id='c76ac3a7b6a64886b42c1ca29adb56ac' log_type='central_heating_state'>
+				<functionalities><point_log id='80df2be12b6c4a5da2a121a0dad9cac0'/></functionalities>
+			</boiler_state>
+			<boiler_state id='e17eeb19153443c697863e5d10988275' log_type='intended_central_heating_state'>
+				<functionalities><point_log id='8f6505222ae941c2b64996fc0b498e2a'/></functionalities>
+			</boiler_state>
+			<fault_code id='eb0a087c495547c9984d03453ea39a93' log_type='open_therm_oem_fault_code'>
+				<functionalities><point_log id='a5559365755d4944b8428ce29017ca97'/></functionalities>
+			</fault_code>
+			<modulation_level id='edc6af7bc03c486fb6e7d64fb025ec78' log_type='modulation_level'>
+				<functionalities><point_log id='c09c711e40c244e9a3f8b08b4f493d67'/></functionalities>
+			</modulation_level>
+			<thermo_meter id='f9ae2df13e5a46bcaeb6ac0366c42387' log_type='return_water_temperature'>
+				<functionalities><point_log id='72093ce129c04553a1d44635f48c1455'/></functionalities>
+			</thermo_meter>
+			<cumulative_chrono_meter id='fd1badb08f1c4a1ab2ac15755c84ea7e' log_type='domestic_hot_water_burner_operation_time'>
+				<functionalities><cumulative_log id='1ee606a5a96548098a9ae14fb6d7ed76'/></functionalities>
+			</cumulative_chrono_meter>
+		</services>
+		<protocols>
+			<open_therm_boiler id='d0c69c8c5f674adda8a499b7737feaaf'>
+				<thermo_extension id='d8ade33622cf4f72a862dfde7ac32e8b'/>
+				<open_therm_version>0.0</open_therm_version>
+				<is_modulating>true</is_modulating>
+				<with_cooling>false</with_cooling>
+				<with_domestic_hot_water>false</with_domestic_hot_water>
+				<with_domestic_hot_water_storage>false</with_domestic_hot_water_storage>
+				<with_master_low_off_and_pump_control>true</with_master_low_off_and_pump_control>
+				<with_secondary_heating_circuit>false</with_secondary_heating_circuit>
+				<smartgrid_control>smart_grid_off</smartgrid_control>
+				<test_mode>off</test_mode>
+			</open_therm_boiler>
+		</protocols>
+	</module>
+</modules>


Test patch:
diff --git a/tests/test_smile.py b/tests/test_smile.py
index 2b9281480..de974cbf5 100644
--- a/tests/test_smile.py
+++ b/tests/test_smile.py
@@ -40,9 +40,9 @@ def _write_json(self, call, data):
             os.path.dirname(__file__), "../fixtures/" + self.smile_setup
         )
         datafile = os.path.join(path, call + ".json")
-        if not os.path.exists(path):
+        if not os.path.exists(path):  # pragma: no cover
             os.mkdir(path)
-        if not os.path.exists(os.path.dirname(datafile)):
+        if not os.path.exists(os.path.dirname(datafile)):  # pragma: no cover
             os.mkdir(os.path.dirname(datafile))
 
         with open(datafile, "w") as fixture_file:
@@ -82,6 +82,9 @@ async def setup_app(
             app.router.add_route(
                 "PUT", "/core/locations{tail:.*}", self.smile_set_temp_or_preset
             )
+            app.router.add_route(
+                "DELETE", "/core/notifications{tail:.*}", self.smile_del_notification
+            )
             app.router.add_route("PUT", "/core/rules{tail:.*}", self.smile_set_schedule)
             app.router.add_route(
                 "PUT", "/core/appliances{tail:.*}", self.smile_set_relay
@@ -340,7 +343,7 @@ async def device_test(self, smile=pw_smile.Smile, testdata=None):
         self._write_json("get_all_devices", device_list)
         self._write_json("notifications", smile.notifications)
 
-        location_list, dummy = smile.scan_thermostats()
+        location_list = smile.thermo_locs
 
         _LOGGER.info("Gateway id = %s", smile.gateway_id)
         _LOGGER.info("Hostname = %s", smile.smile_hostname)
@@ -523,6 +526,7 @@ async def test_connect_legacy_anna(self):
         assert not smile.notifications
 
         await self.device_test(smile, testdata)
+        assert smile.active_device_present
 
         await self.tinker_thermostat(
             smile,
@@ -584,6 +588,7 @@ async def test_connect_legacy_anna_2(self):
         assert not smile.notifications
 
         await self.device_test(smile, testdata)
+        assert smile.active_device_present
 
         await self.tinker_thermostat(
             smile,
@@ -721,6 +726,8 @@ async def test_connect_anna_v4(self):
         assert not smile.notifications
 
         await self.device_test(smile, testdata)
+        assert smile.active_device_present
+
         await self.tinker_thermostat(
             smile,
             "eb5309212bf5407bb143e5bfa3b18aee",
@@ -792,10 +799,10 @@ async def test_connect_anna_without_boiler_fw3(self):
             "a270735e4ccd45239424badc0578a2b1": {
                 "outdoor_temperature": 10.8,
             },
-            # Central
-            "c46b4794d28149699eacf053deedd003": {
-                "heating_state": False,
-            },
+            ## Central
+            # "c46b4794d28149699eacf053deedd003": {
+            #    "heating_state": False,
+            # },
         }
 
         self.smile_setup = "anna_without_boiler_fw3"
@@ -815,6 +822,8 @@ async def test_connect_anna_without_boiler_fw3(self):
         assert not smile.notifications
 
         await self.device_test(smile, testdata)
+        assert not smile.active_device_present
+
         await self.tinker_thermostat(
             smile, "c34c6864216446528e95d88985e714cc", good_schemas=["Test", "Normal"]
         )
@@ -849,10 +858,10 @@ async def test_connect_anna_without_boiler_fw4(self):
             "a270735e4ccd45239424badc0578a2b1": {
                 "outdoor_temperature": 16.6,
             },
-            # Central
-            "c46b4794d28149699eacf053deedd003": {
-                "heating_state": True,
-            },
+            ## Central
+            # "c46b4794d28149699eacf053deedd003": {
+            #    "heating_state": True,
+            # },
         }
 
         self.smile_setup = "anna_without_boiler_fw4"
@@ -872,6 +881,8 @@ async def test_connect_anna_without_boiler_fw4(self):
         assert not smile.notifications
 
         await self.device_test(smile, testdata)
+        assert not smile.active_device_present
+
         await self.tinker_thermostat(
             smile, "c34c6864216446528e95d88985e714cc", good_schemas=["Test", "Normal"]
         )
@@ -932,6 +943,8 @@ async def test_connect_adam_plus_anna(self):
         assert not smile.notifications
 
         await self.device_test(smile, testdata)
+        assert smile.active_device_present
+
         await self.tinker_thermostat(
             smile, "009490cc2f674ce6b576863fbb64f867", good_schemas=["Weekschema"]
         )
@@ -969,6 +982,13 @@ async def test_connect_adam_plus_anna_new(self):
         _LOGGER.info("Basics:")
         _LOGGER.info(" # Assert version")
         assert smile.smile_version[0] == "3.2.4"
+        _LOGGER.info(" # Assert legacy")
+        assert not smile._smile_legacy  # pylint: disable=protected-access
+        _LOGGER.info(" # Assert master thermostat")
+        assert not smile.single_master_thermostat()
+
+        await self.device_test(smile, testdata)
+        assert smile.active_device_present
 
         await self.tinker_switch(
             smile,
@@ -978,7 +998,7 @@ async def test_connect_adam_plus_anna_new(self):
         await self.tinker_switch(
             smile, ["2743216f626f43948deec1f7ab3b3d70"], model="dhw_cm_switch"
         )
-        await self.device_test(smile, testdata)
+
         await smile.close_connection()
         await self.disconnect(server, client)
 
@@ -997,7 +1017,7 @@ async def test_connect_adam_zone_per_device(self):
             "b310b72a0e354bfab43089919b9a88bf": {
                 "setpoint": 21.5,
                 "temperature": 26.2,
-                "valve_position": 100,
+                "valve_position": 0,
             },
             # CV pomp
             "78d1126fc4c743db81b61c20e88342a7": {
@@ -1011,8 +1031,8 @@ async def test_connect_adam_zone_per_device(self):
                 "battery": 67,
             },
             # Adam
-            "90986d591dcd426cae3ec3e8111ff730": {"intended_boiler_temperature": 70.0},
             "fe799307f1624099878210aa0b9f1475": {
+                "heating_state": False,
                 "outdoor_temperature": 7.69,
             },
             # Modem
@@ -1040,6 +1060,8 @@ async def test_connect_adam_zone_per_device(self):
         await smile.delete_notification()
 
         await self.device_test(smile, testdata)
+        assert not smile.active_device_present
+
         await self.tinker_thermostat(
             smile, "c50f167537524366a5af7aa3942feb1e", good_schemas=["GF7  Woonkamer"]
         )
@@ -1104,8 +1126,8 @@ async def test_connect_adam_multiple_devices_per_zone(self):
                 "battery": 67,
             },
             # Adam
-            "90986d591dcd426cae3ec3e8111ff730": {"intended_boiler_temperature": 70.0},
             "fe799307f1624099878210aa0b9f1475": {
+                "heating_state": True,
                 "outdoor_temperature": 7.81,
             },
             # Modem
@@ -1132,6 +1154,8 @@ async def test_connect_adam_multiple_devices_per_zone(self):
         assert "af82e4ccf9c548528166d38e560662a4" in smile.notifications
 
         await self.device_test(smile, testdata)
+        assert not smile.active_device_present
+
         await self.tinker_thermostat(
             smile, "c50f167537524366a5af7aa3942feb1e", good_schemas=["GF7  Woonkamer"]
         )
@@ -1299,6 +1323,8 @@ async def test_connect_anna_heatpump(self):
         assert not smile.notifications
 
         await self.device_test(smile, testdata)
+        assert smile.active_device_present
+
         await smile.close_connection()
         await self.disconnect(server, client)
 
@@ -1341,6 +1367,8 @@ async def test_connect_anna_heatpump_cooling(self):
         assert not smile.notifications
 
         await self.device_test(smile, testdata)
+        assert smile.active_device_present
+
         await smile.close_connection()
         await self.disconnect(server, client)
 
@@ -1395,7 +1423,10 @@ async def test_connect_stretch_v31(self):
         assert smile.smile_version[0] == "3.1.11"
         _LOGGER.info(" # Assert legacy")
         assert smile._smile_legacy  # pylint: disable=protected-access
+        _LOGGER.info(" # Assert no master thermostat")
+        assert smile.single_master_thermostat() is None  # it's not a thermostat :)
 
+        smile.get_all_devices()
         await self.device_test(smile, testdata)
 
         await smile.close_connection()
@@ -1428,9 +1459,12 @@ async def test_connect_stretch_v23(self):
         assert smile.smile_version[0] == "2.3.12"
         _LOGGER.info(" # Assert legacy")
         assert smile._smile_legacy  # pylint: disable=protected-access
+        _LOGGER.info(" # Assert no master thermostat")
+        assert smile.single_master_thermostat() is None  # it's not a thermostat :)
 
         await self.tinker_switch(smile, ["2587a7fcdd7e482dab03fda256076b4b"])
 
+        smile.get_all_devices()
         await self.device_test(smile, testdata)
 
         await smile.close_connection()