DebenOldert 7 月之前
父節點
當前提交
5dbeea82bb
共有 37 個文件被更改,包括 0 次插入3701 次删除
  1. 0 58
      custom_components/msp_integration_101_intermediate/README.md
  2. 0 147
      custom_components/msp_integration_101_intermediate/__init__.py
  3. 0 228
      custom_components/msp_integration_101_intermediate/api.py
  4. 0 131
      custom_components/msp_integration_101_intermediate/base.py
  5. 0 63
      custom_components/msp_integration_101_intermediate/binary_sensor.py
  6. 0 348
      custom_components/msp_integration_101_intermediate/config_flow.py
  7. 0 12
      custom_components/msp_integration_101_intermediate/const.py
  8. 0 99
      custom_components/msp_integration_101_intermediate/coordinator.py
  9. 0 157
      custom_components/msp_integration_101_intermediate/fan.py
  10. 0 179
      custom_components/msp_integration_101_intermediate/light.py
  11. 0 17
      custom_components/msp_integration_101_intermediate/manifest.json
  12. 0 155
      custom_components/msp_integration_101_intermediate/sensor.py
  13. 0 147
      custom_components/msp_integration_101_intermediate/services.py
  14. 0 56
      custom_components/msp_integration_101_intermediate/services.yaml
  15. 0 77
      custom_components/msp_integration_101_intermediate/strings.json
  16. 0 96
      custom_components/msp_integration_101_intermediate/switch.py
  17. 0 77
      custom_components/msp_integration_101_intermediate/translations/en.json
  18. 0 31
      custom_components/msp_integration_101_template/README.md
  19. 0 91
      custom_components/msp_integration_101_template/__init__.py
  20. 0 118
      custom_components/msp_integration_101_template/api.py
  21. 0 112
      custom_components/msp_integration_101_template/binary_sensor.py
  22. 0 191
      custom_components/msp_integration_101_template/config_flow.py
  23. 0 6
      custom_components/msp_integration_101_template/const.py
  24. 0 96
      custom_components/msp_integration_101_template/coordinator.py
  25. 0 17
      custom_components/msp_integration_101_template/manifest.json
  26. 0 126
      custom_components/msp_integration_101_template/sensor.py
  27. 0 41
      custom_components/msp_integration_101_template/strings.json
  28. 0 41
      custom_components/msp_integration_101_template/translations/en.json
  29. 0 111
      custom_components/odido_zyxel_5g/__init__.py
  30. 0 108
      custom_components/odido_zyxel_5g/api.py
  31. 0 130
      custom_components/odido_zyxel_5g/config_flow.py
  32. 0 41
      custom_components/odido_zyxel_5g/const.py
  33. 0 146
      custom_components/odido_zyxel_5g/coordinator.py
  34. 0 11
      custom_components/odido_zyxel_5g/manifest.json
  35. 0 157
      custom_components/odido_zyxel_5g/sensor.py
  36. 0 40
      custom_components/odido_zyxel_5g/strings.json
  37. 0 40
      custom_components/odido_zyxel_5g/translations/en.json

+ 0 - 58
custom_components/msp_integration_101_intermediate/README.md

@@ -1,58 +0,0 @@
-# Integration 101 Intermediate Example
-
-This is a slightly more advanced integration built on top of the Integration 101 Template.  If you are new to writing integrations, it is highly recommended to start with the Integration 101 Template and get your integration working to talk to your api and generate some sensor entities, before adding some of these more advanced elements to control functions of your api/devices.
-
-Even if you are more adept, it still might be worth reviewing the Integration 101 Template first to familiarise yourself with how it is built up to this intermediate example.
-
-### What does it show?
-
-It adds the following things to the original basic example.
-
-- More advanced config flow, demonstrating multi-step configs, using selectors and using your api data in the flow.
-- Services - both integration and entity services.  More about the differences of these below.
-- A base entity class that all entity types inherit, to show how you can save code for common entity properties.
-- Examples of using _attr_* attributes in your entity platform classes to set entity properties and reduce code.
-- Switches, lights and fan entity types
-
-It also allows you to play around with this code to see how those changes impact the integration, knowing you are working from a good start point.
-
-### Does it work?
-
-Yes, it is fully functional in the Home Assistant UI, except the api is mocked, so doesn't do anything for real!  However, if you use the switches and lights, you will see other sensors change as they would in a real situation.
-
-## Further Explanaitions
-
-## Services
-
-So, as stated above, there are 2 types of services, Integration Services and Entity Services.  They are pretty similar in creating a service but have nuances in use and are setup slightly differently.
-
-However, in both of these service types, you need to have a services.yaml file with a set of keys that define some properties like name, description etc and also definitions for your fields which drive how they appear in the UI Developer console.
-
-This Intermediate Example has 3 services:
-- 1 Entity Service
-- 1 Integration Service
-- 1 Integration Service with a Response
-
-along with an example service.yaml file to match, so you can see how to create each one in code.
-
-NOTE:  Home Assistant does come with many built in services to control many functions of devices.  Before diving in to create your own, make sure that one doesn't already exist, otherwise you can make your integration more complicated to use by not supporting the serivces expected by your users.
-
-### Integration Services
-
-These are setup at your integration level and perform any function you wish.
-
-Unlike Entity Serivces, you can have a service that does not require an entity to be selected. For example, if you wanted to send a command to your api, your service can just require the user select the command and it will send it.  They can be used to perform functions on some of your entities but it is easier to use Entity services for that.
-
-### Entity Services
-
-These are setup within your entity platform and are specifically for calling a service on an entity or group of entities.
-
-These services automatially request an entity id, device, area or label and as such, you could not perform the same example as above.
-
-The service will be called on entities that match the selected target (area, device, entity or label) and therefore it can run on multiple entities at the same time.  If that entity does not have the function specified in the service, nothing will happen.  Ie, no action will be performed and no error will be raised.  You can this service being called on multiple entities in this example by adding the same label to the 2 lights or put them in the same area and select that label or area when calling the service. Turn on debug logging to see the devices that got updated.
-
-If you want to use this Entity Service on multiple platforms (ie lights and switches), you have to define it in each platform separately.  It is also recommended that if you do this, make the service definition exactly the same or you will run into issues.
-
-An example of use for this is that you want to send a command to one or many lights on your integration, such as setting an off timer (as per our example in the code).  In which case, you would pick an entity relating to a specific light, or select an area to send the command to all lights in that area.
-
-See within lights.py for a commented code example of this.

+ 0 - 147
custom_components/msp_integration_101_intermediate/__init__.py

@@ -1,147 +0,0 @@
-"""The Integration 101 Template integration.
-
-This shows how to use the requests library to get and use data from an external device over http and
-uses this data to create some binary sensors (of a generic type) and sensors (of multiple types).
-
-Things you need to change
-1. Change the api call in the coordinator async_update_data and the config flow validate input methods.
-2. The constants in const.py that define the api data parameters to set sensors for (and the sensor async_setup_entry logic)
-3. The specific sensor types to match your requirements.
-"""
-
-from __future__ import annotations
-
-from collections.abc import Callable
-from dataclasses import dataclass
-import logging
-
-from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import Platform
-from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import ConfigEntryNotReady
-from homeassistant.helpers.device_registry import DeviceEntry
-from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
-
-from .const import DOMAIN
-from .coordinator import ExampleCoordinator
-from .services import ExampleServicesSetup
-
-_LOGGER = logging.getLogger(__name__)
-
-# ----------------------------------------------------------------------------
-# A list of the different platforms we wish to setup.
-# Add or remove from this list based on your specific need
-# of entity platform types.
-# ----------------------------------------------------------------------------
-PLATFORMS: list[Platform] = [
-    Platform.BINARY_SENSOR,
-    Platform.FAN,
-    Platform.LIGHT,
-    Platform.SENSOR,
-    Platform.SWITCH,
-]
-
-type MyConfigEntry = ConfigEntry[RuntimeData]
-
-
-@dataclass
-class RuntimeData:
-    """Class to hold your data."""
-
-    coordinator: DataUpdateCoordinator
-    cancel_update_listener: Callable
-
-
-async def async_setup_entry(hass: HomeAssistant, config_entry: MyConfigEntry) -> bool:
-    """Set up Example Integration from a config entry."""
-
-    # ----------------------------------------------------------------------------
-    # Initialise the coordinator that manages data updates from your api.
-    # This is defined in coordinator.py
-    # ----------------------------------------------------------------------------
-    coordinator = ExampleCoordinator(hass, config_entry)
-
-    # ----------------------------------------------------------------------------
-    # Perform an initial data load from api.
-    # async_config_entry_first_refresh() is special in that it does not log errors
-    # if it fails.
-    # ----------------------------------------------------------------------------
-    await coordinator.async_config_entry_first_refresh()
-
-    # ----------------------------------------------------------------------------
-    # Test to see if api initialised correctly, else raise ConfigNotReady to make
-    # HA retry setup.
-    # Change this to match how your api will know if connected or successful
-    # update.
-    # ----------------------------------------------------------------------------
-    if not coordinator.data:
-        raise ConfigEntryNotReady
-
-    # ----------------------------------------------------------------------------
-    # Initialise a listener for config flow options changes.
-    # This will be removed automatically if the integraiton is unloaded.
-    # See config_flow for defining an options setting that shows up as configure
-    # on the integration.
-    # If you do not want any config flow options, no need to have listener.
-    # ----------------------------------------------------------------------------
-    cancel_update_listener = config_entry.async_on_unload(
-        config_entry.add_update_listener(_async_update_listener)
-    )
-
-    # ----------------------------------------------------------------------------
-    # Add the coordinator and update listener to your config entry to make
-    # accessible throughout your integration
-    # ----------------------------------------------------------------------------
-    config_entry.runtime_data = RuntimeData(coordinator, cancel_update_listener)
-
-    # ----------------------------------------------------------------------------
-    # Setup platforms (based on the list of entity types in PLATFORMS defined above)
-    # This calls the async_setup method in each of your entity type files.
-    # ----------------------------------------------------------------------------
-    await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
-
-    # ----------------------------------------------------------------------------
-    # Setup global services
-    # This can be done here but included in a seperate file for ease of reading.
-    # See also light.py for entity services examples
-    # ----------------------------------------------------------------------------
-    ExampleServicesSetup(hass, config_entry)
-
-    # Return true to denote a successful setup.
-    return True
-
-
-async def _async_update_listener(hass: HomeAssistant, config_entry: ConfigEntry):
-    """Handle config options update.
-
-    Reload the integration when the options change.
-    Called from our listener created above.
-    """
-    await hass.config_entries.async_reload(config_entry.entry_id)
-
-
-async def async_remove_config_entry_device(
-    hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry
-) -> bool:
-    """Delete device if selected from UI.
-
-    Adding this function shows the delete device option in the UI.
-    Remove this function if you do not want that option.
-    You may need to do some checks here before allowing devices to be removed.
-    """
-    return True
-
-
-async def async_unload_entry(hass: HomeAssistant, config_entry: MyConfigEntry) -> bool:
-    """Unload a config entry.
-
-    This is called when you remove your integration or shutdown HA.
-    If you have created any custom services, they need to be removed here too.
-    """
-
-    # Unload services
-    for service in hass.services.async_services_for_domain(DOMAIN):
-        hass.services.async_remove(DOMAIN, service)
-
-    # Unload platforms and return result
-    return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)

+ 0 - 228
custom_components/msp_integration_101_intermediate/api.py

@@ -1,228 +0,0 @@
-"""API Placeholder.
-
-You should create your api seperately and have it hosted on PYPI.  This is included here for the sole purpose
-of making this example code executable.
-"""
-
-from copy import deepcopy
-import logging
-from typing import Any
-
-import requests
-
-_LOGGER = logging.getLogger(__name__)
-
-MOCK_DATA = [
-    {
-        "device_id": 1,
-        "device_type": "SOCKET",
-        "device_name": "Lounge Socket 1",
-        "device_uid": "0123-4567-8910-1112",
-        "software_version": "2.13",
-        "state": "ON",
-        "voltage": 239,
-        "current": 1.2,
-        "energy_delivered": 3247,
-        "last_reboot": "2024-01-01T10:04:23Z",
-    },
-    {
-        "device_id": 2,
-        "device_type": "SOCKET",
-        "device_name": "Lounge Socket 2",
-        "device_uid": "0123-4567-8910-3723",
-        "software_version": "2.13",
-        "state": "ON",
-        "voltage": 237,
-        "current": 0.1,
-        "energy_delivered": 634,
-        "last_reboot": "2024-03-12T17:33:01Z",
-    },
-    {
-        "device_id": 3,
-        "device_type": "ON_OFF_LIGHT",
-        "device_name": "Lounge Light",
-        "device_uid": "0123-4567-8910-4621",
-        "software_version": "1.30",
-        "state": "ON",
-        "voltage": 237,
-        "current": 0.2,
-        "off_timer": "00:00",
-        "last_reboot": "2023-11-11T09:03:01Z",
-    },
-    {
-        "device_id": 4,
-        "device_type": "DIMMABLE_LIGHT",
-        "device_name": "Kitchen Light",
-        "device_uid": "0123-4967-8940-4691",
-        "software_version": "1.35",
-        "state": "ON",
-        "brightness": 85,
-        "voltage": 237,
-        "current": 1.275,
-        "off_timer": "00:00",
-        "last_reboot": "2023-11-11T09:03:01Z",
-    },
-    {
-        "device_id": 5,
-        "device_type": "TEMP_SENSOR",
-        "device_name": "Kitchen Temp Sensor",
-        "device_uid": "0123-4567-8910-9254",
-        "software_version": "3.00",
-        "temperature": 18.3,
-        "last_reboot": "2024-05-02T19:46:00Z",
-    },
-    {
-        "device_id": 6,
-        "device_type": "TEMP_SENSOR",
-        "device_name": "Lounge Temp Sensor",
-        "device_uid": "0123-4567-8910-9255",
-        "software_version": "1.30",
-        "temperature": 19.2,
-        "last_reboot": "2024-03-12T17:33:01Z",
-    },
-    {
-        "device_id": 7,
-        "device_type": "CONTACT_SENSOR",
-        "device_name": "Kitchen Door Sensor",
-        "device_uid": "0123-4567-8911-6295",
-        "software_version": "1.41",
-        "state": "OPEN",
-    },
-    {
-        "device_id": 8,
-        "device_type": "CONTACT_SENSOR",
-        "device_name": "Lounge Door Sensor",
-        "device_uid": "0123-4567-8911-1753",
-        "software_version": "1.41",
-        "state": "CLOSED",
-    },
-    {
-        "device_id": 9,
-        "device_type": "FAN",
-        "device_name": "Lounge Fan",
-        "device_uid": "0123-4599-1541-1793",
-        "software_version": "2.11",
-        "state": "ON",
-        "oscillating": "OFF",
-        "speed": 2,
-    },
-]
-
-
-class API:
-    """Class for example API."""
-
-    def __init__(self, host: str, user: str, pwd: str, mock: bool = False) -> None:
-        """Initialise."""
-        self.host = host
-        self.user = user
-        self.pwd = pwd
-
-        # For getting and setting the mock data
-        self.mock = mock
-        self.mock_data = deepcopy(MOCK_DATA)
-
-        # Mock auth error if user != test and pwd != 1234
-        if mock and (self.user != "test" or self.pwd != "1234"):
-            raise APIAuthError("Invalid credentials!")
-
-    def get_data(self) -> list[dict[str, Any]]:
-        """Get api data."""
-        if self.mock:
-            return self.get_mock_data()
-        try:
-            r = requests.get(f"http://{self.host}/api", timeout=10)
-            return r.json()
-        except requests.exceptions.ConnectTimeout as err:
-            raise APIConnectionError("Timeout connecting to api") from err
-
-    def set_data(self, device_id: int, parameter: str, value: Any) -> bool:
-        """Set api data."""
-        if self.mock:
-            return self.set_mock_data(device_id, parameter, value)
-        try:
-            data = {parameter, value}
-            r = requests.post(
-                f"http://{self.host}/api/{device_id}", json=data, timeout=10
-            )
-        except requests.exceptions.ConnectTimeout as err:
-            raise APIConnectionError("Timeout connecting to api") from err
-        else:
-            return r.status_code == 200
-
-    # ----------------------------------------------------------------------------
-    # The below methods are used to mimic a real api for the example that changes
-    # its values based on commands from the switches and lights and obvioulsy will
-    # not be needed wiht your real api.
-    # ----------------------------------------------------------------------------
-    def get_mock_data(self) -> dict[str, Any]:
-        """Get mock api data."""
-        return self.mock_data
-
-    def set_mock_data(self, device_id: int, parameter: str, value: Any) -> bool:
-        """Update mock data."""
-        try:
-            device = [
-                devices
-                for devices in self.mock_data
-                if devices.get("device_id") == device_id
-            ][0]
-        except IndexError:
-            # Device id does not exist
-            return False
-
-        other_devices = [
-            devices
-            for devices in self.mock_data
-            if devices.get("device_id") != device_id
-        ]
-
-        # Modify device parameter
-        if device.get(parameter):
-            device[parameter] = value
-        else:
-            # Parameter does not exist on device
-            return False
-
-        # For sockets and lights, modify current values when off/on to mimic
-        # real api and show changing sensors from your actions.
-        if device["device_type"] in ["SOCKET", "ON_OFF_LIGHT", "DIMMABLE_LIGHT"]:
-            if value == "OFF":
-                device["current"] = 0
-            else:
-                original_device = [
-                    devices
-                    for devices in MOCK_DATA
-                    if devices.get("device_id") == device_id
-                ][0]
-                device["current"] = original_device.get("current")
-
-        # For dimmable lights if brightness is set to > 0, set to on
-        if device["device_type"] == "DIMMABLE_LIGHT":
-            if parameter == "brightness":
-                if value > 0:
-                    device["state"] = "ON"
-                    device["current"] = value * 0.015
-                else:
-                    device["state"] = "OFF"
-
-            if parameter == "state":
-                if value == "ON":
-                    device["brightness"] = 100
-                else:
-                    device["brightness"] = 0
-
-        _LOGGER.debug("Device Updated: %s", device)
-
-        # Update mock data
-        self.mock_data = other_devices
-        self.mock_data.append(device)
-        return True
-
-
-class APIAuthError(Exception):
-    """Exception class for auth error."""
-
-
-class APIConnectionError(Exception):
-    """Exception class for connection error."""

+ 0 - 131
custom_components/msp_integration_101_intermediate/base.py

@@ -1,131 +0,0 @@
-"""Base entity which all other entity platform classes can inherit.
-
-As all entity types have a common set of properties, you can
-create a base entity like this and inherit it in all your entity platforms.
-
-This just makes your code more efficient and is totally optional.
-
-See each entity platform (ie sensor.py, switch.py) for how this is inheritted
-and what additional properties and methods you need to add for each entity type.
-
-"""
-
-import logging
-from typing import Any
-
-from homeassistant.core import callback
-from homeassistant.helpers.device_registry import DeviceInfo
-from homeassistant.helpers.update_coordinator import CoordinatorEntity
-
-from .const import DOMAIN
-from .coordinator import ExampleCoordinator
-
-_LOGGER = logging.getLogger(__name__)
-
-
-class ExampleBaseEntity(CoordinatorEntity):
-    """Base Entity Class.
-
-    This inherits a CoordinatorEntity class to register your entites to be updated
-    by your DataUpdateCoordinator when async_update_data is called, either on the scheduled
-    interval or by forcing an update.
-    """
-
-    coordinator: ExampleCoordinator
-
-    # ----------------------------------------------------------------------------
-    # Using attr_has_entity_name = True causes HA to name your entities with the
-    # device name and entity name.  Ie if your name property of your entity is
-    # Voltage and this entity belongs to a device, Lounge Socket, this will name
-    # your entity to be sensor.lounge_socket_voltage
-    #
-    # It is highly recommended (by me) to use this to give a good name structure
-    # to your entities.  However, totally optional.
-    # ----------------------------------------------------------------------------
-    _attr_has_entity_name = True
-
-    def __init__(
-        self, coordinator: ExampleCoordinator, device: dict[str, Any], parameter: str
-    ) -> None:
-        """Initialise entity."""
-        super().__init__(coordinator)
-        self.device = device
-        self.device_id = device["device_id"]
-        self.parameter = parameter
-
-    @callback
-    def _handle_coordinator_update(self) -> None:
-        """Update sensor with latest data from coordinator."""
-        # This method is called by your DataUpdateCoordinator when a successful update runs.
-        self.device = self.coordinator.get_device(self.device_id)
-        _LOGGER.debug(
-            "Updating device: %s, %s",
-            self.device_id,
-            self.coordinator.get_device_parameter(self.device_id, "device_name"),
-        )
-        self.async_write_ha_state()
-
-    @property
-    def device_info(self) -> DeviceInfo:
-        """Return device information."""
-
-        # ----------------------------------------------------------------------------
-        # Identifiers are what group entities into the same device.
-        # If your device is created elsewhere, you can just specify the indentifiers
-        # parameter to link an entity to a device.
-        # If your device connects via another device, add via_device parameter with
-        # the indentifiers of that device.
-        #
-        # Device identifiers should be unique, so use your integration name (DOMAIN)
-        # and a device uuid, mac address or some other unique attribute.
-        # ----------------------------------------------------------------------------
-        return DeviceInfo(
-            name=self.coordinator.get_device_parameter(self.device_id, "device_name"),
-            manufacturer="ACME Manufacturer",
-            model=str(
-                self.coordinator.get_device_parameter(self.device_id, "device_type")
-            )
-            .replace("_", " ")
-            .title(),
-            sw_version=self.coordinator.get_device_parameter(
-                self.device_id, "software_version"
-            ),
-            identifiers={
-                (
-                    DOMAIN,
-                    self.coordinator.get_device_parameter(self.device_id, "device_uid"),
-                )
-            },
-        )
-
-    @property
-    def name(self) -> str:
-        """Return the name of the sensor."""
-        return self.parameter.replace("_", " ").title()
-
-    @property
-    def unique_id(self) -> str:
-        """Return unique id."""
-
-        # ----------------------------------------------------------------------------
-        # All entities must have a unique id across your whole Home Assistant server -
-        # and that also goes for anyone using your integration who may have many other
-        # integrations loaded.
-        #
-        # Think carefully what you want this to be as changing it later will cause HA
-        # to create new entities.
-        #
-        # It is recommended to have your integration name (DOMAIN), some unique id
-        # from your device such as a UUID, MAC address etc (not IP address) and then
-        # something unique to your entity (like name - as this would be unique on a
-        # device)
-        #
-        # If in your situation you have some hub that connects to devices which then
-        # you want to create multiple sensors for each device, you would do something
-        # like.
-        #
-        # f"{DOMAIN}-{HUB_MAC_ADDRESS}-{DEVICE_UID}-{ENTITY_NAME}""
-        #
-        # This is even more important if your integration supports multiple instances.
-        # ----------------------------------------------------------------------------
-        return f"{DOMAIN}-{self.coordinator.get_device_parameter(self.device_id, "device_uid")}-{self.parameter}"

+ 0 - 63
custom_components/msp_integration_101_intermediate/binary_sensor.py

@@ -1,63 +0,0 @@
-"""Binary sensor setup for our Integration."""
-
-import logging
-
-from homeassistant.components.binary_sensor import (
-    BinarySensorDeviceClass,
-    BinarySensorEntity,
-)
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-
-from . import MyConfigEntry
-from .base import ExampleBaseEntity
-from .coordinator import ExampleCoordinator
-
-_LOGGER = logging.getLogger(__name__)
-
-
-async def async_setup_entry(
-    hass: HomeAssistant,
-    config_entry: MyConfigEntry,
-    async_add_entities: AddEntitiesCallback,
-):
-    """Set up the Binary Sensors."""
-    # This gets the data update coordinator from hass.data as specified in your __init__.py
-    # This gets the data update coordinator from the config entry runtime data as specified in your __init__.py
-    coordinator: ExampleCoordinator = config_entry.runtime_data.coordinator
-
-    # ----------------------------------------------------------------------------
-    # Here we are going to add some binary sensors for the contact sensors in our
-    # mock data. So we add an instance of our ExampleBinarySensor class for each
-    # contact sensor we have in our data.
-    # ----------------------------------------------------------------------------
-    binary_sensors = [
-        ExampleBinarySensor(coordinator, device, "state")
-        for device in coordinator.data
-        if device.get("device_type") == "CONTACT_SENSOR"
-    ]
-
-    # Create the binary sensors.
-    async_add_entities(binary_sensors)
-
-
-class ExampleBinarySensor(ExampleBaseEntity, BinarySensorEntity):
-    """Implementation of a sensor.
-
-    This inherits our ExampleBaseEntity to set common properties.
-    See base.py for this class.
-
-    https://developers.home-assistant.io/docs/core/entity/binary-sensor
-    """
-
-    # https://developers.home-assistant.io/docs/core/entity/binary-sensor#available-device-classes
-    _attr_device_class = BinarySensorDeviceClass.DOOR
-
-    @property
-    def is_on(self) -> bool | None:
-        """Return if the binary sensor is on."""
-        # This needs to enumerate to true or false
-        return (
-            self.coordinator.get_device_parameter(self.device_id, self.parameter)
-            == "OPEN"
-        )

+ 0 - 348
custom_components/msp_integration_101_intermediate/config_flow.py

@@ -1,348 +0,0 @@
-"""Config flows for our integration.
-
-This config flow demonstrates many aspects of possible config flows.
-
-Multi step flows
-Menus
-Using your api data in your flow
-"""
-
-from __future__ import annotations
-
-import logging
-from typing import Any
-
-import voluptuous as vol
-
-from homeassistant.config_entries import (
-    ConfigEntry,
-    ConfigFlow,
-    ConfigFlowResult,
-    OptionsFlow,
-)
-from homeassistant.const import (
-    CONF_CHOOSE,
-    CONF_DESCRIPTION,
-    CONF_HOST,
-    CONF_MINIMUM,
-    CONF_PASSWORD,
-    CONF_SCAN_INTERVAL,
-    CONF_SENSORS,
-    CONF_USERNAME,
-)
-from homeassistant.core import HomeAssistant, callback
-from homeassistant.exceptions import HomeAssistantError
-from homeassistant.helpers.selector import selector
-
-from .api import API, APIAuthError, APIConnectionError
-from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, MIN_SCAN_INTERVAL
-from .coordinator import ExampleCoordinator
-
-_LOGGER = logging.getLogger(__name__)
-
-# ----------------------------------------------------------------------------
-# Adjust the data schema to the data that you need
-# ----------------------------------------------------------------------------
-STEP_USER_DATA_SCHEMA = vol.Schema(
-    {
-        vol.Required(CONF_HOST, description={"suggested_value": "10.10.10.1"}): str,
-        vol.Required(CONF_USERNAME, description={"suggested_value": "test"}): str,
-        vol.Required(CONF_PASSWORD, description={"suggested_value": "1234"}): str,
-    }
-)
-
-# ----------------------------------------------------------------------------
-# Example selectors
-# There are lots of selectors available for you to use, described at
-# https://www.home-assistant.io/docs/blueprint/selectors/
-# ----------------------------------------------------------------------------
-STEP_SETTINGS_DATA_SCHEMA = vol.Schema(
-    {
-        vol.Required(CONF_SENSORS): selector(
-            {"entity": {"filter": {"integration": "sun"}}}
-        ),
-        # Take note of translation key and entry in strings.json and translation files.
-        vol.Required(CONF_CHOOSE): selector(
-            {
-                "select": {
-                    "options": ["all", "light", "switch"],
-                    "mode": "dropdown",
-                    "translation_key": "example_selector",
-                }
-            }
-        ),
-        vol.Required(CONF_MINIMUM): selector({"number": {"min": 0, "max": 100}}),
-    }
-)
-
-
-async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
-    """Validate the user input allows us to connect.
-
-    Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
-    """
-    try:
-        # ----------------------------------------------------------------------------
-        # If your api is not async, use the executor to access it
-        # If you cannot connect, raise CannotConnect
-        # If the authentication is wrong, raise InvalidAuth
-        # ----------------------------------------------------------------------------
-        api = API(data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD], mock=True)
-        await hass.async_add_executor_job(api.get_data)
-    except APIAuthError as err:
-        raise InvalidAuth from err
-    except APIConnectionError as err:
-        raise CannotConnect from err
-    return {"title": f"Example Integration - {data[CONF_HOST]}"}
-
-
-async def validate_settings(hass: HomeAssistant, data: dict[str, Any]) -> bool:
-    """Another validation method for our config steps."""
-    return True
-
-
-class ExampleConfigFlow(ConfigFlow, domain=DOMAIN):
-    """Handle a config flow for Example Integration."""
-
-    VERSION = 1
-    _input_data: dict[str, Any]
-    _title: str
-
-    @staticmethod
-    @callback
-    def async_get_options_flow(config_entry):
-        """Get the options flow for this handler.
-
-        Remove this method and the ExampleOptionsFlowHandler class
-        if you do not want any options for your integration.
-        """
-        return ExampleOptionsFlowHandler(config_entry)
-
-    async def async_step_user(
-        self, user_input: dict[str, Any] | None = None
-    ) -> ConfigFlowResult:
-        """Handle the initial step.
-
-        Called when you initiate adding an integration via the UI
-        """
-
-        errors: dict[str, str] = {}
-
-        if user_input is not None:
-            # The form has been filled in and submitted, so process the data provided.
-            try:
-                # ----------------------------------------------------------------------------
-                # Validate that the setup data is valid and if not handle errors.
-                # You can do any validation you want or no validation on each step.
-                # The errors["base"] values match the values in your strings.json and translation files.
-                # ----------------------------------------------------------------------------
-                info = await validate_input(self.hass, user_input)
-            except CannotConnect:
-                errors["base"] = "cannot_connect"
-            except InvalidAuth:
-                errors["base"] = "invalid_auth"
-            except Exception:  # pylint: disable=broad-except
-                _LOGGER.exception("Unexpected exception")
-                errors["base"] = "unknown"
-
-            if "base" not in errors:
-                # Validation was successful, so proceed to the next step.
-
-                # ----------------------------------------------------------------------------
-                # Setting our unique id here just because we have the info at this stage to do that
-                # and it will abort early on in the process if alreay setup.
-                # You can put this in any step however.
-                # ----------------------------------------------------------------------------
-                await self.async_set_unique_id(info.get("title"))
-                self._abort_if_unique_id_configured()
-
-                # Set our title variable here for use later
-                self._title = info["title"]
-
-                # ----------------------------------------------------------------------------
-                # You need to save the input data to a class variable as you go through each step
-                # to ensure it is accessible across all steps.
-                # ----------------------------------------------------------------------------
-                self._input_data = user_input
-
-                # Call the next step
-                return await self.async_step_settings()
-
-        # Show initial form.
-        return self.async_show_form(
-            step_id="user",
-            data_schema=STEP_USER_DATA_SCHEMA,
-            errors=errors,
-            last_step=False,  # Adding last_step True/False decides whether form shows Next or Submit buttons
-        )
-
-    async def async_step_settings(
-        self, user_input: dict[str, Any] | None = None
-    ) -> ConfigFlowResult:
-        """Handle the second step.
-
-        Our second config flow step.
-        Works just the same way as the first step.
-        Except as it is our last step, we create the config entry after any validation.
-        """
-
-        errors: dict[str, str] = {}
-
-        if user_input is not None:
-            # The form has been filled in and submitted, so process the data provided.
-            if not await validate_settings(self.hass, user_input):
-                errors["base"] = "invalid_settings"
-
-            if "base" not in errors:
-                # ----------------------------------------------------------------------------
-                # Validation was successful, so create the config entry.
-                # ----------------------------------------------------------------------------
-                self._input_data.update(user_input)
-                return self.async_create_entry(title=self._title, data=self._input_data)
-
-        # ----------------------------------------------------------------------------
-        # Show settings form.  The step id always needs to match the bit after async_step_ in your method.
-        # Set last_step to True here if it is last step.
-        # ----------------------------------------------------------------------------
-        return self.async_show_form(
-            step_id="settings",
-            data_schema=STEP_SETTINGS_DATA_SCHEMA,
-            errors=errors,
-            last_step=True,
-        )
-
-    async def async_step_reconfigure(
-        self, user_input: dict[str, Any] | None = None
-    ) -> ConfigFlowResult:
-        """Add reconfigure step to allow to reconfigure a config entry.
-
-        This methid displays a reconfigure option in the integration and is
-        different to options.
-        It can be used to reconfigure any of the data submitted when first installed.
-        This is optional and can be removed if you do not want to allow reconfiguration.
-        """
-        errors: dict[str, str] = {}
-        config_entry = self.hass.config_entries.async_get_entry(
-            self.context["entry_id"]
-        )
-
-        if user_input is not None:
-            try:
-                user_input[CONF_HOST] = config_entry.data[CONF_HOST]
-                await validate_input(self.hass, user_input)
-            except CannotConnect:
-                errors["base"] = "cannot_connect"
-            except InvalidAuth:
-                errors["base"] = "invalid_auth"
-            except Exception:  # pylint: disable=broad-except
-                _LOGGER.exception("Unexpected exception")
-                errors["base"] = "unknown"
-            else:
-                return self.async_update_reload_and_abort(
-                    config_entry,
-                    unique_id=config_entry.unique_id,
-                    data={**config_entry.data, **user_input},
-                    reason="reconfigure_successful",
-                )
-        return self.async_show_form(
-            step_id="reconfigure",
-            data_schema=vol.Schema(
-                {
-                    vol.Required(
-                        CONF_USERNAME, default=config_entry.data[CONF_USERNAME]
-                    ): str,
-                    vol.Required(CONF_PASSWORD): str,
-                }
-            ),
-            errors=errors,
-        )
-
-
-class ExampleOptionsFlowHandler(OptionsFlow):
-    """Handles the options flow.
-
-    Here we use an initial menu to select different options forms,
-    and show how to use api data to populate a selector.
-    """
-
-    def __init__(self, config_entry: ConfigEntry) -> None:
-        """Initialize options flow."""
-        self.config_entry = config_entry
-        self.options = dict(config_entry.options)
-
-    async def async_step_init(self, user_input=None):
-        """Handle options flow.
-
-        Display an options menu
-        option ids relate to step function name
-        Also need to be in strings.json and translation files.
-        """
-
-        return self.async_show_menu(
-            step_id="init",
-            menu_options=["option1", "option2"],
-        )
-
-    async def async_step_option1(self, user_input=None):
-        """Handle menu option 1 flow."""
-        if user_input is not None:
-            options = self.config_entry.options | user_input
-            return self.async_create_entry(title="", data=options)
-
-        # ----------------------------------------------------------------------------
-        # It is recommended to prepopulate options fields with default values if
-        # available.
-        # These will be the same default values you use on your coordinator for
-        # setting variable values if the option has not been set.
-        # ----------------------------------------------------------------------------
-        data_schema = vol.Schema(
-            {
-                vol.Optional(
-                    CONF_SCAN_INTERVAL,
-                    default=self.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL),
-                ): (vol.All(vol.Coerce(int), vol.Clamp(min=MIN_SCAN_INTERVAL))),
-                vol.Optional(
-                    CONF_DESCRIPTION,
-                    default=self.options.get(CONF_DESCRIPTION),
-                ): str,
-            }
-        )
-
-        return self.async_show_form(step_id="option1", data_schema=data_schema)
-
-    async def async_step_option2(self, user_input=None):
-        """Handle menu option 2 flow.
-
-        In this option, we show how to use dynamic data in a selector.
-        """
-        if user_input is not None:
-            options = self.config_entry.options | user_input
-            return self.async_create_entry(title="", data=options)
-
-        coordinator: ExampleCoordinator = self.hass.data[DOMAIN][
-            self.config_entry.entry_id
-        ].coordinator
-        devices = coordinator.data
-        data_schema = vol.Schema(
-            {
-                vol.Optional(CONF_CHOOSE, default=devices[0]["device_name"]): selector(
-                    {
-                        "select": {
-                            "options": [device["device_name"] for device in devices],
-                            "mode": "dropdown",
-                            "sort": True,
-                        }
-                    }
-                ),
-            }
-        )
-
-        return self.async_show_form(step_id="option2", data_schema=data_schema)
-
-
-class CannotConnect(HomeAssistantError):
-    """Error to indicate we cannot connect."""
-
-
-class InvalidAuth(HomeAssistantError):
-    """Error to indicate there is invalid auth."""

+ 0 - 12
custom_components/msp_integration_101_intermediate/const.py

@@ -1,12 +0,0 @@
-"""Constants for our integration."""
-
-DOMAIN = "msp_integration_101_intermediate"
-
-DEFAULT_SCAN_INTERVAL = 60
-MIN_SCAN_INTERVAL = 10
-
-RENAME_DEVICE_SERVICE_NAME = "rename_device_service"
-RESPONSE_SERVICE_NAME = "response_service"
-
-SET_OFF_TIMER_ENTITY_SERVICE_NAME = "set_off_timer"
-CONF_OFF_TIME = "off_time"

+ 0 - 99
custom_components/msp_integration_101_intermediate/coordinator.py

@@ -1,99 +0,0 @@
-"""DataUpdateCoordinator for our integration."""
-
-from datetime import timedelta
-import logging
-from typing import Any
-
-from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import (
-    CONF_HOST,
-    CONF_PASSWORD,
-    CONF_SCAN_INTERVAL,
-    CONF_USERNAME,
-)
-from homeassistant.core import DOMAIN, HomeAssistant
-from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
-
-from .api import API, APIConnectionError
-from .const import DEFAULT_SCAN_INTERVAL
-
-_LOGGER = logging.getLogger(__name__)
-
-
-class ExampleCoordinator(DataUpdateCoordinator):
-    """My example coordinator."""
-
-    data: list[dict[str, Any]]
-
-    def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
-        """Initialize coordinator."""
-
-        # Set variables from values entered in config flow setup
-        self.host = config_entry.data[CONF_HOST]
-        self.user = config_entry.data[CONF_USERNAME]
-        self.pwd = config_entry.data[CONF_PASSWORD]
-
-        # set variables from options.  You need a default here in case options have not been set
-        self.poll_interval = config_entry.options.get(
-            CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
-        )
-
-        # Initialise DataUpdateCoordinator
-        super().__init__(
-            hass,
-            _LOGGER,
-            name=f"{DOMAIN} ({config_entry.unique_id})",
-            # Method to call on every update interval.
-            update_method=self.async_update_data,
-            # Polling interval. Will only be polled if you have made your
-            # platform entities, CoordinatorEntities.
-            # Using config option here but you can just use a fixed value.
-            update_interval=timedelta(seconds=self.poll_interval),
-        )
-
-        # Initialise your api here and make available to your integration.
-        self.api = API(host=self.host, user=self.user, pwd=self.pwd, mock=True)
-
-    async def async_update_data(self):
-        """Fetch data from API endpoint.
-
-        This is the place to retrieve and pre-process the data into an appropriate data structure
-        to be used to provide values for all your entities.
-        """
-        try:
-            # ----------------------------------------------------------------------------
-            # Get the data from your api
-            # NOTE: Change this to use a real api call for data
-            # ----------------------------------------------------------------------------
-            data = await self.hass.async_add_executor_job(self.api.get_data)
-        except APIConnectionError as err:
-            _LOGGER.error(err)
-            raise UpdateFailed(err) from err
-        except Exception as err:
-            # This will show entities as unavailable by raising UpdateFailed exception
-            raise UpdateFailed(f"Error communicating with API: {err}") from err
-
-        # What is returned here is stored in self.data by the DataUpdateCoordinator
-        return data
-
-    # ----------------------------------------------------------------------------
-    # Here we add some custom functions on our data coordinator to be called
-    # from entity platforms to get access to the specific data they want.
-    #
-    # These will be specific to your api or yo may not need them at all
-    # ----------------------------------------------------------------------------
-    def get_device(self, device_id: int) -> dict[str, Any]:
-        """Get a device entity from our api data."""
-        try:
-            return [
-                devices for devices in self.data if devices["device_id"] == device_id
-            ][0]
-        except (TypeError, IndexError):
-            # In this case if the device id does not exist you will get an IndexError.
-            # If api did not return any data, you will get TypeError.
-            return None
-
-    def get_device_parameter(self, device_id: int, parameter: str) -> Any:
-        """Get the parameter value of one of our devices from our api data."""
-        if device := self.get_device(device_id):
-            return device.get(parameter)

+ 0 - 157
custom_components/msp_integration_101_intermediate/fan.py

@@ -1,157 +0,0 @@
-"""Fan setup for our Integration."""
-
-import logging
-from typing import Any
-
-from homeassistant.components.fan import FanEntity, FanEntityFeature
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.util.percentage import percentage_to_ranged_value
-
-from . import MyConfigEntry
-from .base import ExampleBaseEntity
-from .coordinator import ExampleCoordinator
-
-_LOGGER = logging.getLogger(__name__)
-
-
-async def async_setup_entry(
-    hass: HomeAssistant,
-    config_entry: MyConfigEntry,
-    async_add_entities: AddEntitiesCallback,
-):
-    """Set up the Fans."""
-    # This gets the data update coordinator from the config entry runtime data as specified in your __init__.py
-    coordinator: ExampleCoordinator = config_entry.runtime_data.coordinator
-
-    # ----------------------------------------------------------------------------
-    # Here we are going to add our fan entity for the fan in our mock data.
-    # ----------------------------------------------------------------------------
-
-    # Fans
-    fans = [
-        ExampleFan(coordinator, device, "state")
-        for device in coordinator.data
-        if device.get("device_type") == "FAN"
-    ]
-
-    # Create the fans.
-    async_add_entities(fans)
-
-
-class ExampleFan(ExampleBaseEntity, FanEntity):
-    """Implementation of a fan.
-
-    This inherits our ExampleBaseEntity to set common properties.
-    See base.py for this class.
-
-    https://developers.home-assistant.io/docs/core/entity/fan/
-    """
-
-    _attr_speed_count = 3
-    _attr_supported_features = FanEntityFeature.OSCILLATE | FanEntityFeature.SET_SPEED
-
-    _speed_parameter = "speed"
-    _oscillating_parameter = "oscillating"
-
-    @property
-    def is_on(self) -> bool | None:
-        """Return if the fan is on."""
-        # This needs to enumerate to true or false
-        return (
-            self.coordinator.get_device_parameter(self.device_id, self.parameter)
-            == "ON"
-        )
-
-    @property
-    def oscillating(self) -> bool | None:
-        """Return whether or not the fan is currently oscillating."""
-        # This needs to enumerate to true or false
-        return (
-            self.coordinator.get_device_parameter(
-                self.device_id, self._oscillating_parameter
-            )
-            == "ON"
-        )
-
-    @property
-    def percentage(self) -> int | None:
-        """Return the current speed as a percentage."""
-        speed = self.coordinator.get_device_parameter(
-            self.device_id, self._speed_parameter
-        )
-        # Need to return a percentage but our fan has speeds 0,1,2,3
-        return int(self.percentage_step * speed)
-
-    async def async_turn_on(
-        self,
-        percentage: int | None = None,
-        preset_mode: str | None = None,
-        **kwargs: Any,
-    ) -> None:
-        """Turn on the fan.
-
-        A turn on command can be sent with or without a %, so we
-        need to check that and turn on and set speed if requested.
-        """
-
-        await self.hass.async_add_executor_job(
-            self.coordinator.api.set_data, self.device_id, self.parameter, "ON"
-        )
-
-        if percentage:
-            self.async_set_fan_speed(percentage)
-        # ----------------------------------------------------------------------------
-        # Use async_refresh on the DataUpdateCoordinator to perform immediate update.
-        # Using self.async_update or self.coordinator.async_request_refresh may delay update due
-        # to trying to batch requests.
-        # ----------------------------------------------------------------------------
-        await self.coordinator.async_refresh()
-
-    async def async_turn_off(self, **kwargs: Any) -> None:
-        """Turn the fan off."""
-
-        await self.hass.async_add_executor_job(
-            self.coordinator.api.set_data, self.device_id, self.parameter, "OFF"
-        )
-        await self.coordinator.async_refresh()
-
-    async def async_set_percentage(self, percentage: int) -> None:
-        """Set the speed of the fan, as a percentage.
-
-        Here we need to apply some logic if % is 0 to turn the fan
-        off instead of setting its speed.  If the fan is off, turn it on with
-        the speed setting, otherwise just set the speed setting.
-        """
-
-        if percentage == 0:
-            await self.async_turn_off()
-        elif not self.is_on:
-            await self.async_turn_on(percentage)
-        else:
-            await self.async_set_fan_speed(percentage)
-            await self.coordinator.async_refresh()
-
-    async def async_oscillate(self, oscillating: bool) -> None:
-        """Oscillate the fan."""
-
-        await self.hass.async_add_executor_job(
-            self.coordinator.api.set_data,
-            self.device_id,
-            self._oscillating_parameter,
-            "ON" if oscillating else "OFF",
-        )
-        await self.coordinator.async_refresh()
-
-    # ----------------------------------------------------------------------------
-    # Added a custom method to make our code simpler
-    # ----------------------------------------------------------------------------
-
-    async def async_set_fan_speed(self, percentage: int) -> None:
-        """Set fan speed."""
-        await self.hass.async_add_executor_job(
-            self.coordinator.api.set_data,
-            self.device_id,
-            self._speed_parameter,
-            percentage_to_ranged_value(1, self.speed_count, percentage),
-        )

+ 0 - 179
custom_components/msp_integration_101_intermediate/light.py

@@ -1,179 +0,0 @@
-"""Light setup for our Integration."""
-
-from datetime import timedelta
-import logging
-from typing import Any
-
-import voluptuous as vol
-
-from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers import config_validation as cv, entity_platform
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-
-from . import MyConfigEntry
-from .base import ExampleBaseEntity
-from .const import CONF_OFF_TIME, SET_OFF_TIMER_ENTITY_SERVICE_NAME
-from .coordinator import ExampleCoordinator
-
-_LOGGER = logging.getLogger(__name__)
-
-
-async def async_setup_entry(
-    hass: HomeAssistant,
-    config_entry: MyConfigEntry,
-    async_add_entities: AddEntitiesCallback,
-):
-    """Set up the Binary Sensors."""
-    # This gets the data update coordinator from the config entry runtime data as specified in your __init__.py
-    coordinator: ExampleCoordinator = config_entry.runtime_data.coordinator
-
-    # ----------------------------------------------------------------------------
-    # Here we are going to add some lights entities for the lights in our mock data.
-    # We have an on/off light and a dimmable light in our mock data, so add each
-    # specific class based on the light type.
-    # ----------------------------------------------------------------------------
-    lights = []
-
-    # On/Off lights
-    lights.extend(
-        [
-            ExampleOnOffLight(coordinator, device, "state")
-            for device in coordinator.data
-            if device.get("device_type") == "ON_OFF_LIGHT"
-        ]
-    )
-
-    # Dimmable lights
-    lights.extend(
-        [
-            ExampleDimmableLight(coordinator, device, "state")
-            for device in coordinator.data
-            if device.get("device_type") == "DIMMABLE_LIGHT"
-        ]
-    )
-
-    # Create the lights.
-    async_add_entities(lights)
-
-    # ----------------------------------------------------------------------------
-    # Add an Entity Service
-    #
-    # This example creates a service to set an off timer for our mock device.
-    # You can have your schema as anything you need and must relate to the entry
-    # in services.yaml.  Best to set your target filters in serivces.yaml to match
-    # what this can be called on, so your users cannot pick inappropriate entities.
-    # If an entity is supplied that does not support this service, nothing will
-    # happen.
-    #
-    # The function async_set_off_timer has to be part of your entity class and
-    # is shown below.
-    #
-    # You will see the off timer sensor on a light update to reflect the time you
-    # set.
-    # ----------------------------------------------------------------------------
-    platform = entity_platform.async_get_current_platform()
-    platform.async_register_entity_service(
-        SET_OFF_TIMER_ENTITY_SERVICE_NAME,
-        {
-            vol.Required(CONF_OFF_TIME): cv.time_period,
-        },
-        "async_set_off_timer",
-    )
-
-
-class ExampleOnOffLight(ExampleBaseEntity, LightEntity):
-    """Implementation of an on/off light.
-
-    This inherits our ExampleBaseEntity to set common properties.
-    See base.py for this class.
-
-    https://developers.home-assistant.io/docs/core/entity/light/
-    """
-
-    _attr_supported_color_modes = {ColorMode.ONOFF}
-    _attr_color_mode = ColorMode.ONOFF
-
-    @property
-    def is_on(self) -> bool | None:
-        """Return if the binary sensor is on."""
-        # This needs to enumerate to true or false
-        return (
-            self.coordinator.get_device_parameter(self.device_id, self.parameter)
-            == "ON"
-        )
-
-    async def async_turn_on(self, **kwargs: Any) -> None:
-        """Turn the entity on."""
-        await self.hass.async_add_executor_job(
-            self.coordinator.api.set_data, self.device_id, self.parameter, "ON"
-        )
-        # ----------------------------------------------------------------------------
-        # Use async_refresh on the DataUpdateCoordinator to perform immediate update.
-        # Using self.async_update or self.coordinator.async_request_refresh may delay update due
-        # to trying to batch requests.
-        # ----------------------------------------------------------------------------
-        await self.coordinator.async_refresh()
-
-    async def async_turn_off(self, **kwargs: Any) -> None:
-        """Turn the entity off."""
-        await self.hass.async_add_executor_job(
-            self.coordinator.api.set_data, self.device_id, self.parameter, "OFF"
-        )
-        # ----------------------------------------------------------------------------
-        # Use async_refresh on the DataUpdateCoordinator to perform immediate update.
-        # Using self.async_update or self.coordinator.async_request_refresh may delay update due
-        # to trying to batch requests.
-        # ----------------------------------------------------------------------------
-        await self.coordinator.async_refresh()
-
-    async def async_set_off_timer(self, off_time: timedelta) -> None:
-        """Handle the set off timer service call.
-
-        Important here to have your service parameters included in your
-        function as they are passed as named parameters.
-        """
-        await self.hass.async_add_executor_job(
-            self.coordinator.api.set_data,
-            self.device_id,
-            "off_timer",
-            ":".join(str(off_time).split(":")[:2]),
-        )
-        # We have made a change to our device, so call a refresh to get updated data.
-        # We use async_request_refresh here to batch the updates in case you select
-        # multiple entities.
-        await self.coordinator.async_request_refresh()
-
-
-class ExampleDimmableLight(ExampleOnOffLight):
-    """Implementation of a dimmable light."""
-
-    _attr_supported_color_modes = {ColorMode.BRIGHTNESS}
-    _attr_color_mode = ColorMode.BRIGHTNESS
-
-    @property
-    def brightness(self) -> int:
-        """Return the brightness of this light between 0..255."""
-        # Our light is in range 0..100, so convert
-        return int(
-            self.coordinator.get_device_parameter(self.device_id, "brightness")
-            * (255 / 100)
-        )
-
-    async def async_turn_on(self, **kwargs: Any) -> None:
-        """Turn the entity on."""
-        if ATTR_BRIGHTNESS in kwargs:
-            brightness = int(kwargs[ATTR_BRIGHTNESS] * (100 / 255))
-            await self.hass.async_add_executor_job(
-                self.coordinator.api.set_data, self.device_id, "brightness", brightness
-            )
-        else:
-            await self.hass.async_add_executor_job(
-                self.coordinator.api.set_data, self.device_id, self.parameter, "ON"
-            )
-        # ----------------------------------------------------------------------------
-        # Use async_refresh on the DataUpdateCoordinator to perform immediate update.
-        # Using self.async_update or self.coordinator.async_request_refresh may delay update due
-        # to trying to batch requests and cause wierd UI behaviour.
-        # ----------------------------------------------------------------------------
-        await self.coordinator.async_refresh()

+ 0 - 17
custom_components/msp_integration_101_intermediate/manifest.json

@@ -1,17 +0,0 @@
-{
-  "domain": "msp_integration_101_intermediate",
-  "name": "MSP Integration 101 Intermediate Example",
-  "codeowners": [
-    "@msp1974"
-  ],
-  "config_flow": true,
-  "dependencies": [],
-  "documentation": "https://github.com/msp1974/HAIntegrationExamples",
-  "homekit": {},
-  "iot_class": "local_polling",
-  "requirements": [],
-  "single_config_entry": false,
-  "ssdp": [],
-  "version": "1.0.1",
-  "zeroconf": []
-}

+ 0 - 155
custom_components/msp_integration_101_intermediate/sensor.py

@@ -1,155 +0,0 @@
-"""Sensor setup for our Integration.
-
-Here we use a different method to define some of our entity classes.
-As, in our example, so much is common, we use our base entity class to define
-many properties, then our base sensor class to define the property to get the
-value of the sensor.
-
-As such, for all our other sensor types, we can just set the _attr_ value to
-keep our code small and easily readable.  You can do this for all entity properties(attributes)
-if you so wish, or mix and match to suit.
-"""
-
-from dataclasses import dataclass
-import logging
-
-from homeassistant.components.sensor import (
-    SensorDeviceClass,
-    SensorEntity,
-    SensorStateClass,
-)
-from homeassistant.const import (
-    UnitOfElectricCurrent,
-    UnitOfElectricPotential,
-    UnitOfEnergy,
-    UnitOfTemperature,
-)
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-
-from . import MyConfigEntry
-from .base import ExampleBaseEntity
-from .coordinator import ExampleCoordinator
-
-_LOGGER = logging.getLogger(__name__)
-
-
-@dataclass
-class SensorTypeClass:
-    """Class for holding sensor type to sensor class."""
-
-    type: str
-    sensor_class: object
-
-
-async def async_setup_entry(
-    hass: HomeAssistant,
-    config_entry: MyConfigEntry,
-    async_add_entities: AddEntitiesCallback,
-):
-    """Set up the Sensors."""
-    # This gets the data update coordinator from the config entry runtime data as specified in your __init__.py
-    coordinator: ExampleCoordinator = config_entry.runtime_data.coordinator
-
-    # ----------------------------------------------------------------------------
-    # Here we enumerate the sensors in your data value from your
-    # DataUpdateCoordinator and add an instance of your sensor class to a list
-    # for each one.
-    # This maybe different in your specific case, depending on how your data is
-    # structured
-    # ----------------------------------------------------------------------------
-
-    sensor_types = [
-        SensorTypeClass("current", ExampleCurrentSensor),
-        SensorTypeClass("energy_delivered", ExampleEnergySensor),
-        SensorTypeClass("off_timer", ExampleOffTimerSensor),
-        SensorTypeClass("temperature", ExampleTemperatureSensor),
-        SensorTypeClass("voltage", ExampleVoltageSensor),
-    ]
-
-    sensors = []
-
-    for sensor_type in sensor_types:
-        sensors.extend(
-            [
-                sensor_type.sensor_class(coordinator, device, sensor_type.type)
-                for device in coordinator.data
-                if device.get(sensor_type.type)
-            ]
-        )
-
-    # Now create the sensors.
-    async_add_entities(sensors)
-
-
-class ExampleBaseSensor(ExampleBaseEntity, SensorEntity):
-    """Implementation of a sensor.
-
-    This inherits our ExampleBaseEntity to set common properties.
-    See base.py for this class.
-
-    https://developers.home-assistant.io/docs/core/entity/sensor
-    """
-
-    @property
-    def native_value(self) -> int | float:
-        """Return the state of the entity."""
-        # Using native value and native unit of measurement, allows you to change units
-        # in Lovelace and HA will automatically calculate the correct value.
-        return self.coordinator.get_device_parameter(self.device_id, self.parameter)
-
-
-class ExampleCurrentSensor(ExampleBaseSensor):
-    """Class to handle current sensors.
-
-    This inherits the ExampleBaseSensor and so uses all the properties and methods
-    from that class and then overrides specific attributes relevant to this sensor type.
-    """
-
-    _attr_device_class = SensorDeviceClass.CURRENT
-    _attr_native_unit_of_measurement = UnitOfElectricCurrent.AMPERE
-    _attr_suggested_display_precision = 2
-
-
-class ExampleEnergySensor(ExampleBaseSensor):
-    """Class to handle energy sensors.
-
-    This inherits the ExampleBaseSensor and so uses all the properties and methods
-    from that class and then overrides specific attributes relevant to this sensor type.
-    """
-
-    _attr_device_class = SensorDeviceClass.ENERGY
-    _attr_state_class = SensorStateClass.TOTAL_INCREASING
-    _attr_native_unit_of_measurement = UnitOfEnergy.WATT_HOUR
-
-
-class ExampleOffTimerSensor(ExampleBaseSensor):
-    """Class to handle off timer sensors.
-
-    This inherits the ExampleBaseSensor and so uses all the properties and methods
-    from that class and then overrides specific attributes relevant to this sensor type.
-    """
-
-
-class ExampleTemperatureSensor(ExampleBaseSensor):
-    """Class to handle temperature sensors.
-
-    This inherits the ExampleBaseSensor and so uses all the properties and methods
-    from that class and then overrides specific attributes relevant to this sensor type.
-    """
-
-    _attr_device_class = SensorDeviceClass.TEMPERATURE
-    _attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
-    _attr_suggested_display_precision = 1
-
-
-class ExampleVoltageSensor(ExampleBaseSensor):
-    """Class to handle voltage sensors.
-
-    This inherits the ExampleBaseSensor and so uses all the properties and methods
-    from that class and then overrides specific attributes relevant to this sensor type.
-    """
-
-    _attr_device_class = SensorDeviceClass.VOLTAGE
-    _attr_native_unit_of_measurement = UnitOfElectricPotential.VOLT
-    _attr_suggested_display_precision = 0

+ 0 - 147
custom_components/msp_integration_101_intermediate/services.py

@@ -1,147 +0,0 @@
-"""Global services file.
-
-This needs to be viewed with the services.yaml file
-to demonstrate the different setup for using these services in the UI
-
-IMPORTANT NOTES:
-To ensure your service runs on the event loop, either make service function async
-or decorate with @callback.  However, ensure that your function is non blocking or,
-if it is, run in the executor.
-Both examples are shown here.  Running services on different threads can cause issues.
-
-https://developers.home-assistant.io/docs/dev_101_services/
-"""
-
-import voluptuous as vol
-
-from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import ATTR_DEVICE_ID, ATTR_NAME
-from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse, callback
-from homeassistant.exceptions import HomeAssistantError
-import homeassistant.helpers.device_registry as dr
-
-from .const import DOMAIN, RENAME_DEVICE_SERVICE_NAME, RESPONSE_SERVICE_NAME
-from .coordinator import ExampleCoordinator
-
-ATTR_TEXT = "text"
-
-# Services schemas
-RENAME_DEVICE_SERVICE_SCHEMA = vol.Schema(
-    {
-        vol.Required(ATTR_DEVICE_ID): int,
-        vol.Required(ATTR_NAME): str,
-    }
-)
-
-RESPONSE_SERVICE_SCHEMA = vol.Schema(
-    {
-        vol.Required(ATTR_DEVICE_ID): int,
-    }
-)
-
-
-class ExampleServicesSetup:
-    """Class to handle Integration Services."""
-
-    def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
-        """Initialise services."""
-        self.hass = hass
-        self.config_entry = config_entry
-        self.coordinator: ExampleCoordinator = config_entry.runtime_data.coordinator
-
-        self.setup_services()
-
-    def setup_services(self):
-        """Initialise the services in Hass."""
-        # ----------------------------------------------------------------------------
-        # A simple definition of a service with 2 parameters, as denoted by the
-        # RENAME_DEVICE_SERVICE_SCHEMA
-        # ----------------------------------------------------------------------------
-        self.hass.services.async_register(
-            DOMAIN,
-            RENAME_DEVICE_SERVICE_NAME,
-            self.rename_device,
-            schema=RENAME_DEVICE_SERVICE_SCHEMA,
-        )
-
-        # ----------------------------------------------------------------------------
-        # The definition here for a response service is the same as before but you
-        # must include supports_response = only/optional
-        # https://developers.home-assistant.io/docs/dev_101_services/#supporting-response-data
-        # ----------------------------------------------------------------------------
-        self.hass.services.async_register(
-            DOMAIN,
-            RESPONSE_SERVICE_NAME,
-            self.async_response_service,
-            schema=RESPONSE_SERVICE_SCHEMA,
-            supports_response=SupportsResponse.ONLY,
-        )
-
-    async def rename_device(self, service_call: ServiceCall) -> None:
-        """Execute rename device service call function.
-
-        This will send a command to the api which will rename the
-        device and then update the device registry to match.
-
-        Data from the service call will be in service_call.data
-        as seen below.
-        """
-        device_id = service_call.data[ATTR_DEVICE_ID]
-        device_name = service_call.data[ATTR_NAME]
-
-        # check for valid device id
-        try:
-            assert self.coordinator.get_device(device_id) is not None
-        except AssertionError as ex:
-            raise HomeAssistantError(
-                "Error calling service: The device ID does not exist"
-            ) from ex
-        else:
-            result = await self.hass.async_add_executor_job(
-                self.coordinator.api.set_data, device_id, "device_name", device_name
-            )
-
-            if result:
-                # ----------------------------------------------------------------------------
-                # In this scenario, we would need to update the device registry name here
-                # as it will not automatically update.
-                # ----------------------------------------------------------------------------
-
-                # Get our device from coordinator data to retrieve its devie_uid as that is
-                # what we used in the devices identifiers.
-                device = self.coordinator.get_device(device_id)
-
-                # Get the device registry
-                device_registry = dr.async_get(self.hass)
-
-                # Get the device entry in the registry by its identifiers.  This is the same as
-                # we used to set them in base.py
-                device_entry = device_registry.async_get_device(
-                    [(DOMAIN, device["device_uid"])]
-                )
-
-                # Update our device entry with the new name.  You will see this change in the UI
-                device_registry.async_update_device(device_entry.id, name=device_name)
-
-            await self.coordinator.async_request_refresh()
-
-    @callback
-    def async_response_service(self, service_call: ServiceCall) -> None:
-        """Execute response service call function.
-
-        This will take a device id and return json data for the
-        devices info on the api.
-
-        If the device does not exist, it will raise an error.
-        """
-        device_id = service_call.data[ATTR_DEVICE_ID]
-        response = self.coordinator.get_device(device_id)
-
-        try:
-            assert response is not None
-        except AssertionError as ex:
-            raise HomeAssistantError(
-                "Error calling service: The device ID does not exist"
-            ) from ex
-        else:
-            return response

+ 0 - 56
custom_components/msp_integration_101_intermediate/services.yaml

@@ -1,56 +0,0 @@
-rename_device_service:
-  name: Example 101 Rename API device
-  description: >
-    Change the name of a device on the example API
-  fields:
-    device_id:
-      name: Device ID
-      description: The device ID to rename
-      example: 1
-      required: true
-      selector:
-        number:
-          min: 1
-          step: 1
-          mode: box
-    name:
-      name: Device name
-      description: New name for the device
-      example: "Kitchen Light 3"
-      required: true
-      selector:
-        text:
-
-response_service:
-  name: Example 101 Response Service
-  description: A simple response service
-  fields:
-    device_id:
-      name: Device ID
-      description: The name of the entity to perform the service on
-      example: 1
-      required: true
-      selector:
-        number:
-          min: 1
-          step: 1
-          mode: box
-
-
-set_off_timer:
-  name: Set off timer
-  decription: Set an off timer for a light
-  target:
-    entity:
-      integration: msp_integration_101_intermediate
-      domain:
-        - light
-  fields:
-    off_time:
-      name: "Turn off in"
-      description: "The time to countdown to turn off"
-      example: "12:00:00"
-      default: "12:00:00"
-      required: true
-      selector:
-        time:

+ 0 - 77
custom_components/msp_integration_101_intermediate/strings.json

@@ -1,77 +0,0 @@
-{
-  "config": {
-    "title": "Advanced Config Flow Example",
-    "abort": {
-      "already_configured": "Device is already configured",
-      "reconfigure_successful": "Reconfiguration successful",
-      "already_in_progress": "A setup is already in progress for this integration"
-    },
-    "error": {
-      "cannot_connect": "Failed to connect",
-      "invalid_auth": "Invalid authentication",
-      "invalid_settings": "Invalid settings",
-      "unknown": "Unexpected error"
-    },
-    "step": {
-      "user": {
-        "title": "Multi Step Config - Step 1",
-        "data": {
-          "host": "Host",
-          "password": "Password",
-          "username": "Username"
-        }
-      },
-      "settings": {
-        "title": "Multi Step Config - Step 2",
-        "data": {
-          "sensors": "Sensor",
-          "choose": "Selector",
-          "minimum": "Minimum Value"
-        }
-      },
-      "reconfigure": {
-        "data": {
-          "host": "Host",
-          "password": "Password",
-          "username": "Username"
-        }
-      }
-    }
-  },
-  "options": {
-    "step": {
-      "init": {
-        "title": "Example2 Options",
-        "description": "Select which options to amend.",
-        "menu_options": {
-          "option1": "Options Set 1",
-          "option2": "Options Set 2"
-        }
-      },
-      "option1": {
-        "title": "Example2 Options",
-        "description": "Option Set 1",
-        "data": {
-          "scan_interval": "Scan Interval (seconds)",
-          "description": "My Description"
-        }
-      },
-      "option2": {
-        "title": "Example2 Options",
-        "description": "Option Set 2",
-        "data": {
-          "chose": "Pick a device"
-        }
-      }
-    }
-  },
-  "selector": {
-    "example_selector": {
-      "options": {
-        "all": "All",
-        "light": "Light",
-        "switch": "Switch"
-      }
-    }
-  }
-}

+ 0 - 96
custom_components/msp_integration_101_intermediate/switch.py

@@ -1,96 +0,0 @@
-"""Switch setup for our Integration."""
-
-import logging
-from typing import Any
-
-from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-
-from . import MyConfigEntry
-from .base import ExampleBaseEntity
-from .coordinator import ExampleCoordinator
-
-_LOGGER = logging.getLogger(__name__)
-
-
-async def async_setup_entry(
-    hass: HomeAssistant,
-    config_entry: MyConfigEntry,
-    async_add_entities: AddEntitiesCallback,
-):
-    """Set up the Binary Sensors."""
-    # This gets the data update coordinator from the config entry runtime data as specified in your __init__.py
-    coordinator: ExampleCoordinator = config_entry.runtime_data.coordinator
-
-    # ----------------------------------------------------------------------------
-    # Here we enumerate the switches in your data value from your
-    # DataUpdateCoordinator and add an instance of your switch class to a list
-    # for each one.
-    # This maybe different in your specific case, depending on how your data is
-    # structured
-    # ----------------------------------------------------------------------------
-
-    switches = [
-        ExampleSwitch(coordinator, device, "state")
-        for device in coordinator.data
-        if device.get("device_type") == "SOCKET"
-    ]
-
-    # Create the binary sensors.
-    async_add_entities(switches)
-
-
-class ExampleSwitch(ExampleBaseEntity, SwitchEntity):
-    """Implementation of a switch.
-
-    This inherits our ExampleBaseEntity to set common properties.
-    See base.py for this class.
-
-    https://developers.home-assistant.io/docs/core/entity/switch
-    """
-
-    _attr_device_class = SwitchDeviceClass.SWITCH
-
-    @property
-    def is_on(self) -> bool | None:
-        """Return if the binary sensor is on."""
-        # This needs to enumerate to true or false
-        return (
-            self.coordinator.get_device_parameter(self.device_id, self.parameter)
-            == "ON"
-        )
-
-    async def async_turn_on(self, **kwargs: Any) -> None:
-        """Turn the entity on."""
-        await self.hass.async_add_executor_job(
-            self.coordinator.api.set_data, self.device_id, self.parameter, "ON"
-        )
-        # ----------------------------------------------------------------------------
-        # Use async_refresh on the DataUpdateCoordinator to perform immediate update.
-        # Using self.async_update or self.coordinator.async_request_refresh may delay update due
-        # to trying to batch requests.
-        # ----------------------------------------------------------------------------
-        await self.coordinator.async_refresh()
-
-    async def async_turn_off(self, **kwargs: Any) -> None:
-        """Turn the entity off."""
-        await self.hass.async_add_executor_job(
-            self.coordinator.api.set_data, self.device_id, self.parameter, "OFF"
-        )
-        # ----------------------------------------------------------------------------
-        # Use async_refresh on the DataUpdateCoordinator to perform immediate update.
-        # Using self.async_update or self.coordinator.async_request_refresh may delay update due
-        # to trying to batch requests.
-        # ----------------------------------------------------------------------------
-        await self.coordinator.async_refresh()
-
-    @property
-    def extra_state_attributes(self):
-        """Return the extra state attributes."""
-        # Add any additional attributes you want on your sensor.
-        attrs = {}
-        attrs["last_rebooted"] = self.coordinator.get_device_parameter(
-            self.device_id, "last_reboot"
-        )
-        return attrs

+ 0 - 77
custom_components/msp_integration_101_intermediate/translations/en.json

@@ -1,77 +0,0 @@
-{
-  "config": {
-    "title": "Advanced Config Flow Example",
-    "abort": {
-      "already_configured": "Device is already configured",
-      "reconfigure_successful": "Reconfiguration successful",
-      "already_in_progress": "A setup is already in progress for this integration"
-    },
-    "error": {
-      "cannot_connect": "Failed to connect",
-      "invalid_auth": "Invalid authentication",
-      "invalid_settings": "Invalid settings",
-      "unknown": "Unexpected error"
-    },
-    "step": {
-      "user": {
-        "title": "Multi Step Config - Step 1",
-        "data": {
-          "host": "Host",
-          "password": "Password",
-          "username": "Username"
-        }
-      },
-      "settings": {
-        "title": "Multi Step Config - Step 2",
-        "data": {
-          "sensors": "Sensor",
-          "choose": "Selector",
-          "minimum": "Minimum Value"
-        }
-      },
-      "reconfigure": {
-        "data": {
-          "host": "Host",
-          "password": "Password",
-          "username": "Username"
-        }
-      }
-    }
-  },
-  "options": {
-    "step": {
-      "init": {
-        "title": "Example2 Options",
-        "description": "Select which options to amend.",
-        "menu_options": {
-          "option1": "Options Set 1",
-          "option2": "Options Set 2"
-        }
-      },
-      "option1": {
-        "title": "Example2 Options",
-        "description": "Option Set 1",
-        "data": {
-          "scan_interval": "Scan Interval (seconds)",
-          "description": "My Description"
-        }
-      },
-      "option2": {
-        "title": "Example2 Options",
-        "description": "Option Set 2",
-        "data": {
-          "chose": "Pick a device"
-        }
-      }
-    }
-  },
-  "selector": {
-    "example_selector": {
-      "options": {
-        "all": "All",
-        "light": "Light",
-        "switch": "Switch"
-      }
-    }
-  }
-}

+ 0 - 31
custom_components/msp_integration_101_template/README.md

@@ -1,31 +0,0 @@
-# Integration 101 Template
-
-So this is your starting point into writing your first Home Assistant integration (or maybe just advancing your knowledge and improving something you have already written).
-
-Well, firstly, I hope you enjoy doing it.  There is something very satisfying to be able to build something into Home Assistant that controls your devices!
-
-So, below is a more detailed explanaition of the major building blocks demonstrated in this example.
-
-If you get stuck, either post a forum question or an issue on this github repo and I'll try my best to help you.  As a note, it always helps if I can see your code, so please make sure you provide a link to that.
-
-1. **Config Flow**
-
-    This is the functionality to provide setup via the UI.  Many new starters to coding, start with a yaml config as it seems easier, but once you understand how to write a config flow (and it is quite simple), this is a much better way to setup and manage your integration from the start.
-
-    See the config_flow.py file with comments to see how it works.  This is much enhanced from the scaffold version to include a reconfigure flow and options flow.
-
-    It is possible (and quite simple) to do multi step flows, which will be covered in another later example.
-
-2. **The DataUpdateCoordinator**
-
-    To me, this should be a default for any integration that gets its data from an api (whether it be a pull (polling) or push type api). It provides much of the functionality to manage polling, receive a websocket message, process your data and update all your entities without you having to do much coding and ensures that all api code is ring fenced within this class.
-
-3. **Devices**
-
-    These are a nice way to group your entities that relate to the same physical device.  Again, this is often very confusing how to create these for an integration.  However, with simple explained code, this can be quite straight forward.
-
-4. **Platform Entities**
-
-    These are your sensors, switches, lights etc, and this example covers the 2 most simple ones of binary sensors, things that only have 2 states, ie On/Off or Open/Closed or Hot/Cold etc and sensors, things that can have many states ie temperature, power, luminance etc.
-
-    There are within Home Assistant things called device classes that describe what your sensor is and set icons, units etc for it.

+ 0 - 91
custom_components/msp_integration_101_template/__init__.py

@@ -1,91 +0,0 @@
-"""The Integration 101 Template integration."""
-
-from __future__ import annotations
-
-from collections.abc import Callable
-from dataclasses import dataclass
-import logging
-
-from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import Platform
-from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import ConfigEntryNotReady
-from homeassistant.helpers.device_registry import DeviceEntry
-from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
-
-from .coordinator import ExampleCoordinator
-
-_LOGGER = logging.getLogger(__name__)
-
-PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR]
-
-type MyConfigEntry = ConfigEntry[RuntimeData]
-
-
-@dataclass
-class RuntimeData:
-    """Class to hold your data."""
-
-    coordinator: DataUpdateCoordinator
-
-
-async def async_setup_entry(hass: HomeAssistant, config_entry: MyConfigEntry) -> bool:
-    """Set up Example Integration from a config entry."""
-
-    # Initialise the coordinator that manages data updates from your api.
-    # This is defined in coordinator.py
-    coordinator = ExampleCoordinator(hass, config_entry)
-
-    # Perform an initial data load from api.
-    # async_config_entry_first_refresh() is special in that it does not log errors if it fails
-    await coordinator.async_config_entry_first_refresh()
-
-    # Test to see if api initialised correctly, else raise ConfigNotReady to make HA retry setup
-    # TODO: Change this to match how your api will know if connected or successful update
-    if not coordinator.api.connected:
-        raise ConfigEntryNotReady
-
-    # Initialise a listener for config flow options changes.
-    # This will be removed automatically if the integraiton is unloaded.
-    # See config_flow for defining an options setting that shows up as configure
-    # on the integration.
-    # If you do not want any config flow options, no need to have listener.
-    config_entry.async_on_unload(
-        config_entry.add_update_listener(_async_update_listener)
-    )
-
-    # Add the coordinator and update listener to config runtime data to make
-    # accessible throughout your integration
-    config_entry.runtime_data = RuntimeData(coordinator)
-
-    # Setup platforms (based on the list of entity types in PLATFORMS defined above)
-    # This calls the async_setup method in each of your entity type files.
-    await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
-
-    # Return true to denote a successful setup.
-    return True
-
-
-async def _async_update_listener(hass: HomeAssistant, config_entry):
-    """Handle config options update."""
-    # Reload the integration when the options change.
-    await hass.config_entries.async_reload(config_entry.entry_id)
-
-
-async def async_remove_config_entry_device(
-    hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry
-) -> bool:
-    """Delete device if selected from UI."""
-    # Adding this function shows the delete device option in the UI.
-    # Remove this function if you do not want that option.
-    # You may need to do some checks here before allowing devices to be removed.
-    return True
-
-
-async def async_unload_entry(hass: HomeAssistant, config_entry: MyConfigEntry) -> bool:
-    """Unload a config entry."""
-    # This is called when you remove your integration or shutdown HA.
-    # If you have created any custom services, they need to be removed here too.
-
-    # Unload platforms and return result
-    return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)

+ 0 - 118
custom_components/msp_integration_101_template/api.py

@@ -1,118 +0,0 @@
-"""API Placeholder.
-
-You should create your api seperately and have it hosted on PYPI.  This is included here for the sole purpose
-of making this example code executable.
-"""
-
-from dataclasses import dataclass
-from enum import StrEnum
-import logging
-from random import choice, randrange
-
-_LOGGER = logging.getLogger(__name__)
-
-
-class DeviceType(StrEnum):
-    """Device types."""
-
-    TEMP_SENSOR = "temp_sensor"
-    DOOR_SENSOR = "door_sensor"
-    OTHER = "other"
-
-
-DEVICES = [
-    {"id": 1, "type": DeviceType.TEMP_SENSOR},
-    {"id": 2, "type": DeviceType.TEMP_SENSOR},
-    {"id": 3, "type": DeviceType.TEMP_SENSOR},
-    {"id": 4, "type": DeviceType.TEMP_SENSOR},
-    {"id": 1, "type": DeviceType.DOOR_SENSOR},
-    {"id": 2, "type": DeviceType.DOOR_SENSOR},
-    {"id": 3, "type": DeviceType.DOOR_SENSOR},
-    {"id": 4, "type": DeviceType.DOOR_SENSOR},
-]
-
-
-@dataclass
-class Device:
-    """API device."""
-
-    device_id: int
-    device_unique_id: str
-    device_type: DeviceType
-    name: str
-    state: int | bool
-
-
-class API:
-    """Class for example API."""
-
-    def __init__(self, host: str, user: str, pwd: str) -> None:
-        """Initialise."""
-        self.host = host
-        self.user = user
-        self.pwd = pwd
-        self.connected: bool = False
-
-    @property
-    def controller_name(self) -> str:
-        """Return the name of the controller."""
-        return self.host.replace(".", "_")
-
-    def connect(self) -> bool:
-        """Connect to api."""
-        if self.user == "test" and self.pwd == "1234":
-            self.connected = True
-            return True
-        raise APIAuthError("Error connecting to api. Invalid username or password.")
-
-    def disconnect(self) -> bool:
-        """Disconnect from api."""
-        self.connected = False
-        return True
-
-    def get_devices(self) -> list[Device]:
-        """Get devices on api."""
-        return [
-            Device(
-                device_id=device.get("id"),
-                device_unique_id=self.get_device_unique_id(
-                    device.get("id"), device.get("type")
-                ),
-                device_type=device.get("type"),
-                name=self.get_device_name(device.get("id"), device.get("type")),
-                state=self.get_device_value(device.get("id"), device.get("type")),
-            )
-            for device in DEVICES
-        ]
-
-    def get_device_unique_id(self, device_id: str, device_type: DeviceType) -> str:
-        """Return a unique device id."""
-        if device_type == DeviceType.DOOR_SENSOR:
-            return f"{self.controller_name}_D{device_id}"
-        if device_type == DeviceType.TEMP_SENSOR:
-            return f"{self.controller_name}_T{device_id}"
-        return f"{self.controller_name}_Z{device_id}"
-
-    def get_device_name(self, device_id: str, device_type: DeviceType) -> str:
-        """Return the device name."""
-        if device_type == DeviceType.DOOR_SENSOR:
-            return f"DoorSensor{device_id}"
-        if device_type == DeviceType.TEMP_SENSOR:
-            return f"TempSensor{device_id}"
-        return f"OtherSensor{device_id}"
-
-    def get_device_value(self, device_id: str, device_type: DeviceType) -> int | bool:
-        """Get device random value."""
-        if device_type == DeviceType.DOOR_SENSOR:
-            return choice([True, False])
-        if device_type == DeviceType.TEMP_SENSOR:
-            return randrange(15, 28)
-        return randrange(1, 10)
-
-
-class APIAuthError(Exception):
-    """Exception class for auth error."""
-
-
-class APIConnectionError(Exception):
-    """Exception class for connection error."""

+ 0 - 112
custom_components/msp_integration_101_template/binary_sensor.py

@@ -1,112 +0,0 @@
-"""Interfaces with the Integration 101 Template api sensors."""
-
-import logging
-
-from homeassistant.components.binary_sensor import (
-    BinarySensorDeviceClass,
-    BinarySensorEntity,
-)
-from homeassistant.core import HomeAssistant, callback
-from homeassistant.helpers.device_registry import DeviceInfo
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.helpers.update_coordinator import CoordinatorEntity
-
-from . import MyConfigEntry
-from .api import Device, DeviceType
-from .const import DOMAIN
-from .coordinator import ExampleCoordinator
-
-_LOGGER = logging.getLogger(__name__)
-
-
-async def async_setup_entry(
-    hass: HomeAssistant,
-    config_entry: MyConfigEntry,
-    async_add_entities: AddEntitiesCallback,
-):
-    """Set up the Binary Sensors."""
-    # This gets the data update coordinator from the config entry runtime data as specified in your __init__.py
-    coordinator: ExampleCoordinator = config_entry.runtime_data.coordinator
-
-    # Enumerate all the binary sensors in your data value from your DataUpdateCoordinator and add an instance of your binary sensor class
-    # to a list for each one.
-    # This maybe different in your specific case, depending on how your data is structured
-    binary_sensors = [
-        ExampleBinarySensor(coordinator, device)
-        for device in coordinator.data.devices
-        if device.device_type == DeviceType.DOOR_SENSOR
-    ]
-
-    # Create the binary sensors.
-    async_add_entities(binary_sensors)
-
-
-class ExampleBinarySensor(CoordinatorEntity, BinarySensorEntity):
-    """Implementation of a sensor."""
-
-    def __init__(self, coordinator: ExampleCoordinator, device: Device) -> None:
-        """Initialise sensor."""
-        super().__init__(coordinator)
-        self.device = device
-        self.device_id = device.device_id
-
-    @callback
-    def _handle_coordinator_update(self) -> None:
-        """Update sensor with latest data from coordinator."""
-        # This method is called by your DataUpdateCoordinator when a successful update runs.
-        self.device = self.coordinator.get_device_by_id(
-            self.device.device_type, self.device_id
-        )
-        _LOGGER.debug("Device: %s", self.device)
-        self.async_write_ha_state()
-
-    @property
-    def device_class(self) -> str:
-        """Return device class."""
-        # https://developers.home-assistant.io/docs/core/entity/binary-sensor#available-device-classes
-        return BinarySensorDeviceClass.DOOR
-
-    @property
-    def device_info(self) -> DeviceInfo:
-        """Return device information."""
-        # Identifiers are what group entities into the same device.
-        # If your device is created elsewhere, you can just specify the indentifiers parameter.
-        # If your device connects via another device, add via_device parameter with the indentifiers of that device.
-        return DeviceInfo(
-            name=f"ExampleDevice{self.device.device_id}",
-            manufacturer="ACME Manufacturer",
-            model="Door&Temp v1",
-            sw_version="1.0",
-            identifiers={
-                (
-                    DOMAIN,
-                    f"{self.coordinator.data.controller_name}-{self.device.device_id}",
-                )
-            },
-        )
-
-    @property
-    def name(self) -> str:
-        """Return the name of the sensor."""
-        return self.device.name
-
-    @property
-    def is_on(self) -> bool | None:
-        """Return if the binary sensor is on."""
-        # This needs to enumerate to true or false
-        return self.device.state
-
-    @property
-    def unique_id(self) -> str:
-        """Return unique id."""
-        # All entities must have a unique id.  Think carefully what you want this to be as
-        # changing it later will cause HA to create new entities.
-        return f"{DOMAIN}-{self.device.device_unique_id}"
-
-    @property
-    def extra_state_attributes(self):
-        """Return the extra state attributes."""
-        # Add any additional attributes you want on your sensor.
-        attrs = {}
-        attrs["extra_info"] = "Extra Info"
-        return attrs

+ 0 - 191
custom_components/msp_integration_101_template/config_flow.py

@@ -1,191 +0,0 @@
-"""Config flow for Integration 101 Template integration."""
-
-from __future__ import annotations
-
-import logging
-from typing import Any
-
-import voluptuous as vol
-
-from homeassistant.config_entries import (
-    ConfigEntry,
-    ConfigFlow,
-    ConfigFlowResult,
-    OptionsFlow,
-)
-from homeassistant.const import (
-    CONF_HOST,
-    CONF_PASSWORD,
-    CONF_SCAN_INTERVAL,
-    CONF_USERNAME,
-)
-from homeassistant.core import HomeAssistant, callback
-from homeassistant.exceptions import HomeAssistantError
-
-from .api import API, APIAuthError, APIConnectionError
-from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, MIN_SCAN_INTERVAL
-
-_LOGGER = logging.getLogger(__name__)
-
-# TODO adjust the data schema to the data that you need
-STEP_USER_DATA_SCHEMA = vol.Schema(
-    {
-        vol.Required(CONF_HOST, description={"suggested_value": "10.10.10.1"}): str,
-        vol.Required(CONF_USERNAME, description={"suggested_value": "test"}): str,
-        vol.Required(CONF_PASSWORD, description={"suggested_value": "1234"}): str,
-    }
-)
-
-
-async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
-    """Validate the user input allows us to connect.
-
-    Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
-    """
-    # TODO validate the data can be used to set up a connection.
-
-    # If your PyPI package is not built with async, pass your methods
-    # to the executor:
-    # await hass.async_add_executor_job(
-    #     your_validate_func, data[CONF_USERNAME], data[CONF_PASSWORD]
-    # )
-
-    api = API(data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD])
-    try:
-        await hass.async_add_executor_job(api.connect)
-        # If you cannot connect, raise CannotConnect
-        # If the authentication is wrong, raise InvalidAuth
-    except APIAuthError as err:
-        raise InvalidAuth from err
-    except APIConnectionError as err:
-        raise CannotConnect from err
-    return {"title": f"Example Integration - {data[CONF_HOST]}"}
-
-
-class ExampleConfigFlow(ConfigFlow, domain=DOMAIN):
-    """Handle a config flow for Example Integration."""
-
-    VERSION = 1
-    _input_data: dict[str, Any]
-
-    @staticmethod
-    @callback
-    def async_get_options_flow(config_entry):
-        """Get the options flow for this handler."""
-        # Remove this method and the ExampleOptionsFlowHandler class
-        # if you do not want any options for your integration.
-        return ExampleOptionsFlowHandler(config_entry)
-
-    async def async_step_user(
-        self, user_input: dict[str, Any] | None = None
-    ) -> ConfigFlowResult:
-        """Handle the initial step."""
-        # Called when you initiate adding an integration via the UI
-        errors: dict[str, str] = {}
-
-        if user_input is not None:
-            # The form has been filled in and submitted, so process the data provided.
-            try:
-                # Validate that the setup data is valid and if not handle errors.
-                # The errors["base"] values match the values in your strings.json and translation files.
-                info = await validate_input(self.hass, user_input)
-            except CannotConnect:
-                errors["base"] = "cannot_connect"
-            except InvalidAuth:
-                errors["base"] = "invalid_auth"
-            except Exception:  # pylint: disable=broad-except
-                _LOGGER.exception("Unexpected exception")
-                errors["base"] = "unknown"
-
-            if "base" not in errors:
-                # Validation was successful, so create a unique id for this instance of your integration
-                # and create the config entry.
-                await self.async_set_unique_id(info.get("title"))
-                self._abort_if_unique_id_configured()
-                return self.async_create_entry(title=info["title"], data=user_input)
-
-        # Show initial form.
-        return self.async_show_form(
-            step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
-        )
-
-    async def async_step_reconfigure(
-        self, user_input: dict[str, Any] | None = None
-    ) -> ConfigFlowResult:
-        """Add reconfigure step to allow to reconfigure a config entry."""
-        # This methid displays a reconfigure option in the integration and is
-        # different to options.
-        # It can be used to reconfigure any of the data submitted when first installed.
-        # This is optional and can be removed if you do not want to allow reconfiguration.
-        errors: dict[str, str] = {}
-        config_entry = self.hass.config_entries.async_get_entry(
-            self.context["entry_id"]
-        )
-
-        if user_input is not None:
-            try:
-                user_input[CONF_HOST] = config_entry.data[CONF_HOST]
-                await validate_input(self.hass, user_input)
-            except CannotConnect:
-                errors["base"] = "cannot_connect"
-            except InvalidAuth:
-                errors["base"] = "invalid_auth"
-            except Exception:  # pylint: disable=broad-except
-                _LOGGER.exception("Unexpected exception")
-                errors["base"] = "unknown"
-            else:
-                return self.async_update_reload_and_abort(
-                    config_entry,
-                    unique_id=config_entry.unique_id,
-                    data={**config_entry.data, **user_input},
-                    reason="reconfigure_successful",
-                )
-        return self.async_show_form(
-            step_id="reconfigure",
-            data_schema=vol.Schema(
-                {
-                    vol.Required(
-                        CONF_USERNAME, default=config_entry.data[CONF_USERNAME]
-                    ): str,
-                    vol.Required(CONF_PASSWORD): str,
-                }
-            ),
-            errors=errors,
-        )
-
-
-class ExampleOptionsFlowHandler(OptionsFlow):
-    """Handles the options flow."""
-
-    def __init__(self, config_entry: ConfigEntry) -> None:
-        """Initialize options flow."""
-        self.config_entry = config_entry
-        self.options = dict(config_entry.options)
-
-    async def async_step_init(self, user_input=None):
-        """Handle options flow."""
-        if user_input is not None:
-            options = self.config_entry.options | user_input
-            return self.async_create_entry(title="", data=options)
-
-        # It is recommended to prepopulate options fields with default values if available.
-        # These will be the same default values you use on your coordinator for setting variable values
-        # if the option has not been set.
-        data_schema = vol.Schema(
-            {
-                vol.Required(
-                    CONF_SCAN_INTERVAL,
-                    default=self.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL),
-                ): (vol.All(vol.Coerce(int), vol.Clamp(min=MIN_SCAN_INTERVAL))),
-            }
-        )
-
-        return self.async_show_form(step_id="init", data_schema=data_schema)
-
-
-class CannotConnect(HomeAssistantError):
-    """Error to indicate we cannot connect."""
-
-
-class InvalidAuth(HomeAssistantError):
-    """Error to indicate there is invalid auth."""

+ 0 - 6
custom_components/msp_integration_101_template/const.py

@@ -1,6 +0,0 @@
-"""Constants for the Integration 101 Template integration."""
-
-DOMAIN = "msp_integration_101_template"
-
-DEFAULT_SCAN_INTERVAL = 60
-MIN_SCAN_INTERVAL = 10

+ 0 - 96
custom_components/msp_integration_101_template/coordinator.py

@@ -1,96 +0,0 @@
-"""Integration 101 Template integration using DataUpdateCoordinator."""
-
-from dataclasses import dataclass
-from datetime import timedelta
-import logging
-
-from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import (
-    CONF_HOST,
-    CONF_PASSWORD,
-    CONF_SCAN_INTERVAL,
-    CONF_USERNAME,
-)
-from homeassistant.core import DOMAIN, HomeAssistant
-from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
-
-from .api import API, APIAuthError, Device, DeviceType
-from .const import DEFAULT_SCAN_INTERVAL
-
-_LOGGER = logging.getLogger(__name__)
-
-
-@dataclass
-class ExampleAPIData:
-    """Class to hold api data."""
-
-    controller_name: str
-    devices: list[Device]
-
-
-class ExampleCoordinator(DataUpdateCoordinator):
-    """My example coordinator."""
-
-    data: ExampleAPIData
-
-    def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
-        """Initialize coordinator."""
-
-        # Set variables from values entered in config flow setup
-        self.host = config_entry.data[CONF_HOST]
-        self.user = config_entry.data[CONF_USERNAME]
-        self.pwd = config_entry.data[CONF_PASSWORD]
-
-        # set variables from options.  You need a default here incase options have not been set
-        self.poll_interval = config_entry.options.get(
-            CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
-        )
-
-        # Initialise DataUpdateCoordinator
-        super().__init__(
-            hass,
-            _LOGGER,
-            name=f"{DOMAIN} ({config_entry.unique_id})",
-            # Method to call on every update interval.
-            update_method=self.async_update_data,
-            # Polling interval. Will only be polled if there are subscribers.
-            # Using config option here but you can just use a value.
-            update_interval=timedelta(seconds=self.poll_interval),
-        )
-
-        # Initialise your api here
-        self.api = API(host=self.host, user=self.user, pwd=self.pwd)
-
-    async def async_update_data(self):
-        """Fetch data from API endpoint.
-
-        This is the place to pre-process the data to lookup tables
-        so entities can quickly look up their data.
-        """
-        try:
-            if not self.api.connected:
-                await self.hass.async_add_executor_job(self.api.connect)
-            devices = await self.hass.async_add_executor_job(self.api.get_devices)
-        except APIAuthError as err:
-            _LOGGER.error(err)
-            raise UpdateFailed(err) from err
-        except Exception as err:
-            # This will show entities as unavailable by raising UpdateFailed exception
-            raise UpdateFailed(f"Error communicating with API: {err}") from err
-
-        # What is returned here is stored in self.data by the DataUpdateCoordinator
-        return ExampleAPIData(self.api.controller_name, devices)
-
-    def get_device_by_id(
-        self, device_type: DeviceType, device_id: int
-    ) -> Device | None:
-        """Return device by device id."""
-        # Called by the binary sensors and sensors to get their updated data from self.data
-        try:
-            return [
-                device
-                for device in self.data.devices
-                if device.device_type == device_type and device.device_id == device_id
-            ][0]
-        except IndexError:
-            return None

+ 0 - 17
custom_components/msp_integration_101_template/manifest.json

@@ -1,17 +0,0 @@
-{
-  "domain": "msp_integration_101_template",
-  "name": "MSP Integration 101 Template Example",
-  "codeowners": [
-    "@msp1974"
-  ],
-  "config_flow": true,
-  "dependencies": [],
-  "documentation": "https://github.com/msp1974/HAIntegrationExamples",
-  "homekit": {},
-  "iot_class": "local_polling",
-  "requirements": [],
-  "single_config_entry": false,
-  "ssdp": [],
-  "version": "1.0.1",
-  "zeroconf": []
-}

+ 0 - 126
custom_components/msp_integration_101_template/sensor.py

@@ -1,126 +0,0 @@
-"""Interfaces with the Integration 101 Template api sensors."""
-
-import logging
-
-from homeassistant.components.sensor import (
-    SensorDeviceClass,
-    SensorEntity,
-    SensorStateClass,
-)
-from homeassistant.const import UnitOfTemperature
-from homeassistant.core import HomeAssistant, callback
-from homeassistant.helpers.device_registry import DeviceInfo
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.helpers.update_coordinator import CoordinatorEntity
-
-from . import MyConfigEntry
-from .api import Device, DeviceType
-from .const import DOMAIN
-from .coordinator import ExampleCoordinator
-
-_LOGGER = logging.getLogger(__name__)
-
-
-async def async_setup_entry(
-    hass: HomeAssistant,
-    config_entry: MyConfigEntry,
-    async_add_entities: AddEntitiesCallback,
-):
-    """Set up the Sensors."""
-    # This gets the data update coordinator from the config entry runtime data as specified in your __init__.py
-    coordinator: ExampleCoordinator = config_entry.runtime_data.coordinator
-
-    # Enumerate all the sensors in your data value from your DataUpdateCoordinator and add an instance of your sensor class
-    # to a list for each one.
-    # This maybe different in your specific case, depending on how your data is structured
-    sensors = [
-        ExampleSensor(coordinator, device)
-        for device in coordinator.data.devices
-        if device.device_type == DeviceType.TEMP_SENSOR
-    ]
-
-    # Create the sensors.
-    async_add_entities(sensors)
-
-
-class ExampleSensor(CoordinatorEntity, SensorEntity):
-    """Implementation of a sensor."""
-
-    def __init__(self, coordinator: ExampleCoordinator, device: Device) -> None:
-        """Initialise sensor."""
-        super().__init__(coordinator)
-        self.device = device
-        self.device_id = device.device_id
-
-    @callback
-    def _handle_coordinator_update(self) -> None:
-        """Update sensor with latest data from coordinator."""
-        # This method is called by your DataUpdateCoordinator when a successful update runs.
-        self.device = self.coordinator.get_device_by_id(
-            self.device.device_type, self.device_id
-        )
-        _LOGGER.debug("Device: %s", self.device)
-        self.async_write_ha_state()
-
-    @property
-    def device_class(self) -> str:
-        """Return device class."""
-        # https://developers.home-assistant.io/docs/core/entity/sensor/#available-device-classes
-        return SensorDeviceClass.TEMPERATURE
-
-    @property
-    def device_info(self) -> DeviceInfo:
-        """Return device information."""
-        # Identifiers are what group entities into the same device.
-        # If your device is created elsewhere, you can just specify the indentifiers parameter.
-        # If your device connects via another device, add via_device parameter with the indentifiers of that device.
-        return DeviceInfo(
-            name=f"ExampleDevice{self.device.device_id}",
-            manufacturer="ACME Manufacturer",
-            model="Door&Temp v1",
-            sw_version="1.0",
-            identifiers={
-                (
-                    DOMAIN,
-                    f"{self.coordinator.data.controller_name}-{self.device.device_id}",
-                )
-            },
-        )
-
-    @property
-    def name(self) -> str:
-        """Return the name of the sensor."""
-        return self.device.name
-
-    @property
-    def native_value(self) -> int | float:
-        """Return the state of the entity."""
-        # Using native value and native unit of measurement, allows you to change units
-        # in Lovelace and HA will automatically calculate the correct value.
-        return float(self.device.state)
-
-    @property
-    def native_unit_of_measurement(self) -> str | None:
-        """Return unit of temperature."""
-        return UnitOfTemperature.CELSIUS
-
-    @property
-    def state_class(self) -> str | None:
-        """Return state class."""
-        # https://developers.home-assistant.io/docs/core/entity/sensor/#available-state-classes
-        return SensorStateClass.MEASUREMENT
-
-    @property
-    def unique_id(self) -> str:
-        """Return unique id."""
-        # All entities must have a unique id.  Think carefully what you want this to be as
-        # changing it later will cause HA to create new entities.
-        return f"{DOMAIN}-{self.device.device_unique_id}"
-
-    @property
-    def extra_state_attributes(self):
-        """Return the extra state attributes."""
-        # Add any additional attributes you want on your sensor.
-        attrs = {}
-        attrs["extra_info"] = "Extra Info"
-        return attrs

+ 0 - 41
custom_components/msp_integration_101_template/strings.json

@@ -1,41 +0,0 @@
-{
-  "config": {
-    "title": "Integration 101 Template Integration",
-    "abort": {
-      "already_configured": "Device is already configured",
-      "reconfigure_successful": "Reconfiguration successful"
-    },
-    "error": {
-      "cannot_connect": "Failed to connect",
-      "invalid_auth": "Invalid authentication",
-      "unknown": "Unexpected error"
-    },
-    "step": {
-      "user": {
-        "data": {
-          "host": "Host",
-          "password": "Password",
-          "username": "Username"
-        }
-      },
-      "reconfigure": {
-        "data": {
-          "host": "Host",
-          "password": "Password",
-          "username": "Username"
-        }
-      }
-    }
-  },
-  "options": {
-    "step": {
-      "init": {
-        "data": {
-          "scan_interval": "Scan Interval (seconds)"
-        },
-        "description": "Amend your options.",
-        "title": "Example Integration Options"
-      }
-    }
-  }
-}

+ 0 - 41
custom_components/msp_integration_101_template/translations/en.json

@@ -1,41 +0,0 @@
-{
-  "config": {
-    "title": "Integration 101 Template Integration",
-    "abort": {
-      "already_configured": "Device is already configured",
-      "reconfigure_successful": "Reconfiguration successful"
-    },
-    "error": {
-      "cannot_connect": "Failed to connect",
-      "invalid_auth": "Invalid authentication",
-      "unknown": "Unexpected error"
-    },
-    "step": {
-      "user": {
-        "data": {
-          "host": "Host",
-          "password": "Password",
-          "username": "Username"
-        }
-      },
-      "reconfigure": {
-        "data": {
-          "host": "Host",
-          "password": "Password",
-          "username": "Username"
-        }
-      }
-    }
-  },
-  "options": {
-    "step": {
-      "init": {
-        "data": {
-          "scan_interval": "Scan Interval (seconds)"
-        },
-        "description": "Amend your options.",
-        "title": "Example Integration Options"
-      }
-    }
-  }
-}

+ 0 - 111
custom_components/odido_zyxel_5g/__init__.py

@@ -1,111 +0,0 @@
-"""
-Custom integration to integrate ZYXEL router statistics
-for Odido Klik & Klaar internet subscriptions.
-"""
-
-import logging
-from datetime import timedelta
-
-from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import (
-    CONF_IP_ADDRESS,
-    CONF_USERNAME,
-    CONF_PASSWORD,
-    CONF_SCAN_INTERVAL,
-    Platform,
-)
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers import entity_registry as er
-from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from homeassistant.helpers.entity import DeviceInfo
-
-
-from .api import RouterApiClient
-from .const import (DEFAULT_SCAN_INTERVAL,
-                    DOMAIN,
-                    EP_DEVICESTATUS,
-                    API_SCHEMA)
-
-from .coordinator import RouterDataUpdateCoordinator
-
-PLATFORMS: list[Platform] = [
-    Platform.BINARY_SENSOR,
-    Platform.DEVICE_TRACKER,
-    Platform.SENSOR
-]
-
-_LOGGER: logging.Logger = logging.getLogger(__package__)
-
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
-    """Set up this integration using UI."""
-    hass.data.setdefault(DOMAIN, {})
-
-    ip = entry.data.get(CONF_IP_ADDRESS)
-    user = entry.data.get(CONF_USERNAME)
-    password = entry.data.get(CONF_PASSWORD)
-    scan_interval_seconds = entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
-    
-    scan_interval = timedelta(seconds=scan_interval_seconds)
-
-    session = async_get_clientsession(hass)
-    client = RouterApiClient(ip=ip,
-                             user=user,
-                             password=password,
-                             session=session)
-
-    _LOGGER.debug(
-        "Set up entry, with scan_interval of %s seconds",
-        scan_interval_seconds,
-    )
-
-    client.async_login()
-
-    data = await client.async_query_api(oid=EP_DEVICESTATUS)
-
-    info = data['DeviceInfo']
-
-    device_info = DeviceInfo(
-        configuration_url=f'{API_SCHEMA}://{ip}',
-        identifiers={(DOMAIN, entry.entry_id)},
-        model=info['ModelName'],
-        manufacturer=info['Manufacturer'],
-        name=info['Description'],
-        sw_version=info['SoftwareVersion'],
-        hw_version=info['HardwareVersion'],
-        model_id=info['ProductClass'],
-        serial_number=['SerialNumber'],
-    )
-
-    hass.data[DOMAIN][entry.entry_id] = coordinator = RouterDataUpdateCoordinator(
-        hass=hass,
-        client=client,
-        device_info=device_info,
-        scan_interval=scan_interval,
-    )
-
-    await coordinator.async_config_entry_first_refresh()
-
-    await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
-    entry.async_on_unload(entry.add_update_listener(async_reload_entry))
-
-    return True
-
-
-async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
-    """Handle removal of an entry."""
-    if unloaded := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
-        hass.data[DOMAIN].pop(entry.entry_id)
-    return unloaded
-
-
-async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
-    """Reload config entry."""
-    await async_unload_entry(hass, entry)
-    await async_setup_entry(hass, entry)
-
-
-async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
-    """Migrate old entry."""
-    _LOGGER.warning("Migrating from version %s", config_entry.version)
-
-    return True

+ 0 - 108
custom_components/odido_zyxel_5g/api.py

@@ -1,108 +0,0 @@
-"""ZYXEL API Client"""
-
-import asyncio
-import base64
-
-import aiohttp
-
-from .const import (API_TIMEOUT,
-                    API_SCHEMA,
-                    API_BASE_PATH,
-                    API_LOGIN_PATH,
-                    LOGIN_PAYLOAD,
-                    KEY_RESULT,
-                    VAL_SUCCES,
-                    KEY_OBJECT)
-
-
-class RouterApiClientError(Exception):
-    """Exception to indicate a general API error."""
-
-
-class RouterApiClientCommunicationError(RouterApiClientError):
-    """Exception to indicate a communication error."""
-
-
-class RouterApiClientLoginError(RouterApiClientError):
-    """Exception to indicate an api key error."""
-
-
-class RouterApiClientResponseError(RouterApiClientError):
-    """Exception to indicate a response error."""
-
-
-class RouterApiClient:
-    """Router API wrapper for ZYXEL routers"""
-
-    def __init__(
-        self,
-        ip: str,
-        user: str,
-        password: str,
-        session: aiohttp.ClientSession,
-    ) -> None:
-        """ZYXEL API Client."""
-        self.ip = ip
-        self.user = user
-        self.password = password
-        self._session = session
-
-    async def async_login(self) -> bool:
-        """Login and obtain the session cookie"""
-
-        payload = LOGIN_PAYLOAD.copy()
-        payload['Input_Account'] = self.user
-        payload['Input_Passwd'] = base64.b64encode(
-            self.password.encode('utf-8')).decode('utf-8')
-
-        response = await self._session.post(
-            f'{API_SCHEMA}://{self.ip}{API_LOGIN_PATH}',
-            json=payload)
-        
-        if response.ok:
-            try:
-                data = response.json()
-
-                if 'result' in data:
-                    if data['result'] == 'ZCFG_SUCCESS':
-                        return True
-                    else:
-                        raise RouterApiClientLoginError('Login failed')
-                else:
-                    raise RouterApiClientResponseError('Key "result" not set in response')
-                    
-            except Exception as json_exception:
-                raise RouterApiClientResponseError(f'Unable to decode login response') \
-                    from json_exception
-            
-        raise RouterApiClientCommunicationError(
-            f'Error connecting to router. Status: {response.status}')
-    
-    async def async_query_api(self,
-                              oid: str) -> dict:
-        """Query an authenticated API endpoint"""
-        try:
-            async with asyncio.timeout(API_TIMEOUT):
-                response = await self._session.get(
-                    f'{API_SCHEMA}://{self.endpoint}{API_BASE_PATH}',
-                    params={'oid': oid})
-
-                if response.ok:
-                    try:
-                        data: dict = await response.json()
-
-                        if data.get(KEY_RESULT, None) == VAL_SUCCES:
-                            return data.get(KEY_OBJECT, [{}])[0]
-                        else:
-                            raise RouterApiClientResponseError(f'Response returned error')
-
-                    except Exception as json_exception:
-                        raise RouterApiClientResponseError(f'Unable to decode JSON') \
-                            from json_exception
-                else:
-                    raise RouterApiClientCommunicationError(
-                        f'Error retrieving API. Status: {response.status}')
-
-        except Exception as exception:
-            raise RouterApiClientCommunicationError('Unable to connect to router API') \
-                from exception

+ 0 - 130
custom_components/odido_zyxel_5g/config_flow.py

@@ -1,130 +0,0 @@
-"""Adds config flow for ZYXEL."""
-
-import logging
-from typing import Any
-
-from homeassistant.config_entries import (
-    ConfigEntry,
-    ConfigFlow,
-    FlowResult,
-    OptionsFlow,
-)
-from homeassistant.const import (
-    CONF_SCAN_INTERVAL,
-    CONF_NAME,
-    CONF_USERNAME,
-    CONF_PASSWORD,
-    CONF_IP_ADDRESS
-)
-from homeassistant.core import callback
-from homeassistant.helpers.aiohttp_client import async_create_clientsession
-from homeassistant.helpers.selector import (TextSelector,
-                                            TextSelectorConfig,
-                                            TextSelectorType)
-import homeassistant.helpers.config_validation as cv
-import voluptuous as vol
-
-from .api import (
-    RouterApiClient,
-    RouterApiClientLoginError,
-    RouterApiClientCommunicationError,
-    RouterApiClientResponseError,
-)
-from .const import (DEFAULT_SCAN_INTERVAL,
-                    DOMAIN,
-                    DEFAULT_IP,
-                    DEFAULT_USER)
-
-_LOGGER: logging.Logger = logging.getLogger(__package__)
-
-
-class RouterFlowHandler(ConfigFlow, domain=DOMAIN):
-    """Config flow for Odido Router."""
-
-    VERSION = 1
-
-    async def async_step_user(
-        self, user_input: dict[str, Any] | None = None
-    ) -> FlowResult:
-        """Handle a flow initialized by the user."""
-        _errors = {}
-        if user_input is not None:
-            try:
-                await self._validate_user_input(
-                    user_input[CONF_NAME],
-                    user_input[CONF_IP_ADDRESS],
-                    user_input[CONF_USERNAME],
-                    user_input[CONF_PASSWORD],
-                )
-            except RouterApiClientCommunicationError as exception:
-                _LOGGER.error(exception)
-                _errors["base"] = "connection"
-            except RouterApiClientLoginError as exception:
-                _LOGGER.error(exception)
-                _errors["base"] = "login"
-            except RouterApiClientResponseError as exception:
-                _LOGGER.error(exception)
-                _errors["base"] = "response"
-            else:
-                return self.async_create_entry(
-                    title=user_input[CONF_NAME], data=user_input
-                )
-
-        return self.async_show_form(
-            step_id="user",
-            data_schema=vol.Schema(
-                {
-                    vol.Required(CONF_NAME, default='5G router'): str,
-                    vol.Required(
-                        CONF_IP_ADDRESS, default=DEFAULT_IP
-                    ): str,
-                    vol.Required(
-                        CONF_USERNAME, default=DEFAULT_USER
-                    ): TextSelector(TextSelectorConfig(type=TextSelectorType.TEXT)),
-                    vol.Required(CONF_PASSWORD
-                    ): TextSelector(TextSelectorConfig(type=TextSelectorType.PASSWORD))
-                }
-            ),
-            errors={},
-        )
-
-    async def _validate_user_input(self, ip: str, user: str, password: str):
-        """Validate user input."""
-        session = async_create_clientsession(self.hass)
-        client = RouterApiClient(ip=ip,
-                                 user=user,
-                                 password=password,
-                                 session=session)
-        await client.async_login()
-
-    @staticmethod
-    @callback
-    def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
-        return RouterOptionsFlowHandler()
-
-
-class RouterOptionsFlowHandler(OptionsFlow):
-    """Router config flow options handler."""
-
-    async def async_step_init(
-        self, user_input: dict[str, Any] | None = None
-    ) -> FlowResult:
-        """Manage the options."""
-        if user_input is not None:
-            return self.async_create_entry(
-                title=self.config_entry.data.get(CONF_NAME), data=user_input
-            )
-
-        return self.async_show_form(
-            step_id="init",
-            data_schema=vol.Schema(
-                {
-                    vol.Required(
-                        CONF_SCAN_INTERVAL,
-                        default=self.config_entry.options.get(
-                            CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
-                        ),
-                    ): vol.All(vol.Coerce(int), vol.Range(min=30, max=3600))
-                }
-            ),
-        )

+ 0 - 41
custom_components/odido_zyxel_5g/const.py

@@ -1,41 +0,0 @@
-"""Constants for ZYXEL"""
-
-from typing import Final
-
-# API
-API_SCHEMA: Final[str] = 'https'
-API_BASE_PATH: Final[str] = '/cgi-bin/DAL'
-API_LOGIN_PATH: Final[str] = '/UserLogin'
-API_TIMEOUT: Final = 10
-API_TIMEZONE: Final = "Europe/Amsterdam"
-
-# Endpoints
-EP_CELLINFO: Final[str] = 'status'
-EP_LANINFO: Final[str] = 'lanhosts'
-EP_DEVICESTATUS: Final[str] = 'cardpage_status'
-
-# Keys & values
-KEY_RESULT: Final[str] = 'result'
-KEY_OBJECT: Final[str] = 'Object'
-VAL_SUCCES: Final[str] = 'ZCFG_SUCCESS'
-
-# Base component constants.
-DOMAIN: Final = "Odido"
-NAME: Final = "ZYXEL"
-SUPPLIER: Final = "Odido"
-VERSION: Final = "0.0.1"
-
-# Defaults
-DEFAULT_IP: Final[str] = '192.168.1.1'
-DEFAULT_USER: Final[str] = 'admin'
-DEFAULT_NAME: Final[str] = NAME
-DEFAULT_SCAN_INTERVAL: Final = 60
-
-# Payloads
-LOGIN_PAYLOAD: dict = {
-    'Input_Account': None,
-    'Input_Passwd': None,
-    'currLang': 'en',
-    'RememberPassword': 0,
-    'SHA512_password': False
-}

+ 0 - 146
custom_components/odido_zyxel_5g/coordinator.py

@@ -1,146 +0,0 @@
-"""Update coordinator for """
-
-from datetime import datetime, timedelta
-import logging
-import re
-import asyncio
-from typing import Any
-
-from homeassistant.config_entries import ConfigEntry
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity import DeviceInfo
-from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
-from homeassistant.helpers.typing import StateType
-from homeassistant.util import dt
-
-from .api import RouterApiClient
-from .const import (API_TIMEZONE, 
-                    DOMAIN,
-                    EP_CELLINFO,
-                    EP_DEVICESTATUS,
-                    EP_LANINFO)
-
-_LOGGER: logging.Logger = logging.getLogger(__package__)
-
-
-class RouterDataUpdateCoordinator(DataUpdateCoordinator):
-    """Class to manage fetching data from the API."""
-
-    config_entry: ConfigEntry
-
-    def __init__(
-        self,
-        hass: HomeAssistant,
-        client: RouterApiClient,
-        device_info: DeviceInfo,
-        scan_interval: timedelta,
-    ) -> None:
-        """Initialize"""
-        self.api = client
-        self.device_info = device_info
-
-        super().__init__(
-            hass=hass,
-            logger=_LOGGER,
-            name=DOMAIN,
-            update_interval=scan_interval,
-        )
-
-    async def _async_update_data(self) -> dict:
-        """Update data via library"""
-        try:
-            await self.api.async_login()
-
-            # Get API endpoints
-            endpoints = [EP_CELLINFO,
-                         EP_DEVICESTATUS,
-                         EP_LANINFO]
-            
-            results = await asyncio.gather(
-                *[self.api.async_query_api(oid=endpoint) for endpoint in endpoints],
-                return_exceptions=True)
-            
-            _LOGGER.debug(results)
-
-        except Exception as exception:
-            _LOGGER.error("Update failed! - %s", exception)
-            raise UpdateFailed() from exception
-
-
-    def get_value(self, endpoint: str, path: list[int | str], default=None) -> StateType:
-        """
-        Get a value from the data by a given path.
-        When the value is absent, the default (None) will be returned and an error will be logged.
-        """
-        value = self.data
-
-        try:
-            for key in path:
-                value = value[key]
-
-            value_type = type(value).__name__
-
-            if value_type in ["int", "float", "str"]:
-                _LOGGER.debug(
-                    "Path %s returns a %s (value = %s)", path, value_type, value
-                )
-            else:
-                _LOGGER.debug("Path %s returns a %s", path, value_type)
-
-            return value
-        except (IndexError, KeyError):
-            _LOGGER.warning("Can't find a value for %s in the API response", path)
-            return default
-
-    def get_value_datetime(
-        self, path: list[int | str], default=None
-    ) -> datetime | None:
-        """
-        Get a datetime value from the data by a given path.
-        When the value is absent, the default (None) will be returned and an error will be logged.
-        """
-        timezone = dt.get_time_zone(API_TIMEZONE)
-        value = self.get_value(path, default)
-
-        # Timestamp.
-        if isinstance(value, int):
-            if value > 0:
-                _LOGGER.debug("convert %s to datetime (from timestamp)", value)
-                return datetime.fromtimestamp(value, tz=timezone)
-
-            return default
-
-        # Time.
-        if re.match(r"^\d{2}:\d{2}$", value):
-            _LOGGER.debug("convert %s to datetime (from time HH:MM)", value)
-            time_array = value.split(":")
-            today = datetime.now(tz=timezone)
-            return today.replace(
-                hour=int(time_array[0]),
-                minute=int(time_array[1]),
-                second=0,
-                microsecond=0,
-            )
-
-        # Date.
-        if re.match(r"^\d{2}-\d{2}-\d{4}$", value):
-            _LOGGER.debug("convert %s to datetime (from date DD-MM-YYYY)", value)
-            return datetime.strptime(value, "%d-%m-%Y").replace(tzinfo=timezone)
-
-        # Date and time.
-        if re.match(r"^\d{2}-\d{2}-\d{4} \d{2}:\d{2}:\d{2}$", value):
-            _LOGGER.debug(
-                "convert %s to datetime (from date and time DD-MM-YYYY HH:MM:SS)", value
-            )
-            return datetime.strptime(value, "%d-%m-%Y %H:%M:%S").replace(
-                tzinfo=timezone
-            )
-
-        # Date and time without seconds.
-        if re.match(r"^\d{2}-\d{2}-\d{4} \d{2}:\d{2}$", value):
-            _LOGGER.debug(
-                "convert %s to datetime (from date and time DD-MM-YYYY HH:MM)", value
-            )
-            return datetime.strptime(value, "%d-%m-%Y %H:%M").replace(tzinfo=timezone)
-
-        return default

+ 0 - 11
custom_components/odido_zyxel_5g/manifest.json

@@ -1,11 +0,0 @@
-{
-    "domain": "odido",
-    "name": "Odido Klik & Klaar",
-    "codeowners": ["@debenoldert"],
-    "config_flow": true,
-    "documentation": "https://github.com/DebenOldert/odido_5g_router/blob/master/README.md",
-    "integration_type": "device",
-    "iot_class": "local_polling",
-    "issue_tracker": "https://github.com/DebenOldert/odido_5g_router/issues",
-    "version": "0.0.1"
-  }

+ 0 - 157
custom_components/odido_zyxel_5g/sensor.py

@@ -1,157 +0,0 @@
-"""Sensor platform for knmi."""
-
-from collections.abc import Callable
-from dataclasses import dataclass
-from datetime import datetime
-from typing import Any
-
-from homeassistant.components.sensor import (
-    SensorDeviceClass,
-    SensorEntity,
-    SensorEntityDescription,
-    SensorStateClass,
-)
-from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import (
-    CONF_NAME,
-    PERCENTAGE,
-    UnitOfSoundPressure,
-    UnitOfDataRate
-)
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity import EntityCategory
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.helpers.typing import StateType
-from homeassistant.helpers.update_coordinator import CoordinatorEntity
-
-from .const import (DOMAIN,
-                    EP_CELLINFO,
-                    EP_DEVICESTATUS,
-                    EP_LANINFO)
-from .coordinator import RouterDataUpdateCoordinator
-
-
-@dataclass(kw_only=True, frozen=True)
-class RouterSensorDescription(SensorEntityDescription):
-    """Class describing Router sensor entities."""
-
-    value_fn: Callable[[dict[str, Any]], StateType | datetime | None]
-    attr_fn: Callable[[dict[str, Any]], dict[str, Any]] = lambda _: {}
-
-
-DESCRIPTIONS: list[RouterSensorDescription] = [
-    RouterSensorDescription(
-        key='rssi',
-        icon='mdi:wifi-check',
-        value_fn=lambda coordinator: coordinator.get_value(EP_CELLINFO, ["CellIntfInfo", "RSSI"]),
-        native_unit_of_measurement=UnitOfSoundPressure.WEIGHTED_DECIBEL_A,
-        device_class=SensorDeviceClass.SOUND_PRESSURE,
-        state_class=SensorStateClass.MEASUREMENT,
-        translation_key='rssi',
-        entity_registry_enabled_default=True,
-    ),
-    RouterSensorDescription(
-        key='rsrq',
-        icon='mdi:wifi-arrow-up-down',
-        value_fn=lambda coordinator: coordinator.get_value(EP_CELLINFO, ["CellIntfInfo", "X_ZYXEL_RSRQ"]),
-        native_unit_of_measurement=UnitOfSoundPressure.DECIBEL,
-        device_class=SensorDeviceClass.SOUND_PRESSURE,
-        state_class=SensorStateClass.MEASUREMENT,
-        translation_key='rsrq',
-        entity_registry_enabled_default=False
-    ),
-    RouterSensorDescription(
-        key='rsrp',
-        icon='mdi:wifi-arrow-down',
-        value_fn=lambda coordinator: coordinator.get_value(EP_CELLINFO, ["CellIntfInfo", "X_ZYXEL_RSRP"]),
-        native_unit_of_measurement=UnitOfSoundPressure.WEIGHTED_DECIBEL_A,
-        device_class=SensorDeviceClass.SOUND_PRESSURE,
-        state_class=SensorStateClass.MEASUREMENT,
-        translation_key='rsrp',
-        entity_registry_enabled_default=False
-    ),
-    RouterSensorDescription(
-        key='sinr',
-        icon='mdi:wifi-alert',
-        value_fn=lambda coordinator: coordinator.get_value(EP_CELLINFO, ["CellIntfInfo", "X_ZYXEL_SINR"]),
-        state_class=SensorStateClass.MEASUREMENT,
-        translation_key='sinr',
-        entity_registry_enabled_default=False
-    ),
-    RouterSensorDescription(
-        key='network_technology',
-        icon='mdi:radio-tower',
-        value_fn=lambda coordinator: coordinator.get_value(EP_CELLINFO, ["CellIntfInfo", "CurrentAccessTechnology"]),
-        translation_key='network_technology',
-        entity_registry_enabled_default=True
-    ),
-    RouterSensorDescription(
-        key='network_band',
-        icon='mdi:signal-5g',
-        value_fn=lambda coordinator: coordinator.get_value(EP_CELLINFO, ["CellIntfInfo", "CurrentAccessTechnology"]),
-        translation_key='network_band',
-        entity_registry_enabled_default=False
-    ),
-    # RouterSensorDescription(
-    #     key='network_devices',
-    #     state_class=SensorStateClass.MEASUREMENT,
-    #     translation_key='network_devices',
-    #     entity_registry_enabled_default=True
-    # )
-]
-
-
-async def async_setup_entry(
-    hass: HomeAssistant,
-    entry: ConfigEntry,
-    async_add_entities: AddEntitiesCallback,
-) -> None:
-    """Set up Router sensors based on a config entry."""
-    conf_name = entry.data.get(CONF_NAME)
-    coordinator = hass.data[DOMAIN][entry.entry_id]
-
-    entities: list[RouterSensor] = []
-
-    # Add all sensors described above.
-    for description in DESCRIPTIONS:
-        entities.append(
-            RouterSensor(
-                conf_name=conf_name,
-                coordinator=coordinator,
-                description=description,
-            )
-        )
-
-    async_add_entities(entities)
-
-
-class RouterSensor(CoordinatorEntity[RouterDataUpdateCoordinator], SensorEntity):
-    """Defines a Router sensor."""
-
-    _attr_has_entity_name = True
-    entity_description: RouterSensorDescription
-
-    def __init__(
-        self,
-        conf_name: str,
-        coordinator: RouterDataUpdateCoordinator,
-        description: SensorEntityDescription,
-    ) -> None:
-        """Initialize Router sensor."""
-        super().__init__(coordinator=coordinator)
-
-        self._attr_attribution = self.coordinator.get_value(["api", 0, "bron"])
-        self._attr_device_info = coordinator.device_info
-        self._attr_unique_id = f"{conf_name}_{description.key}".lower()
-
-        self.entity_description = description
-
-    @property
-    def native_value(self) -> StateType:
-        """Return the state."""
-        return self.entity_description.value_fn(self.coordinator)
-
-    @property
-    def extra_state_attributes(self) -> dict[str, Any]:
-        """Return the state attributes."""
-        return self.entity_description.attr_fn(self.coordinator)

+ 0 - 40
custom_components/odido_zyxel_5g/strings.json

@@ -1,40 +0,0 @@
-{
-  "config": {
-    "step": {
-      "user": {
-        "title": "Odido 5G router",
-        "description": "Read statistics from the 5G router supplied with the Odido 'Klik & Klaar' internet subscription",
-        "data": {
-          "ip_address": "IP address router",
-          "username": "Username",
-          "password": "Password",
-          "name": "Name of the router"
-        }
-      }
-    },
-    "error": {
-      "connection": "Could not connect to the router, check if the IP address is correct.",
-      "login": "Invalid username or password.",
-      "response": "Unknown response returned by the router, check debug logs."
-    }
-  },
-  "options": {
-    "step": {
-      "init": {
-        "data": {
-          "scan_interval": "Refresh interval (seconds)"
-        }
-      }
-    }
-  },
-  "entity": {
-    "sensor": {
-      "rssi": { "name": "Received Signal Strength Indicator" },
-      "rsrq": { "name": "Received Signal Received Quality" },
-      "rsrp": { "name": "Received Signal Received Power" },
-      "sinr": { "name": "Signal To Noise Ratio" },
-      "network_technology": { "name": "Network Technology" },
-      "network_band": { "name": "Network Band" }
-    }
-  }
-}

+ 0 - 40
custom_components/odido_zyxel_5g/translations/en.json

@@ -1,40 +0,0 @@
-{
-  "config": {
-    "step": {
-      "user": {
-        "title": "Odido 5G router",
-        "description": "Read statistics from the 5G router supplied with the Odido 'Klik & Klaar' internet subscription",
-        "data": {
-          "ip_address": "IP address router",
-          "username": "Username",
-          "password": "Password",
-          "name": "Name of the router"
-        }
-      }
-    },
-    "error": {
-      "connection": "Could not connect to the router, check if the IP address is correct.",
-      "login": "Invalid username or password.",
-      "response": "Unknown response returned by the router, check debug logs."
-    }
-  },
-  "options": {
-    "step": {
-      "init": {
-        "data": {
-          "scan_interval": "Refresh interval (seconds)"
-        }
-      }
-    }
-  },
-  "entity": {
-    "sensor": {
-      "rssi": { "name": "Received Signal Strength Indicator" },
-      "rsrq": { "name": "Received Signal Received Quality" },
-      "rsrp": { "name": "Received Signal Received Power" },
-      "sinr": { "name": "Signal To Noise Ratio" },
-      "network_technology": { "name": "Network Technology" },
-      "network_band": { "name": "Network Band" }
-    }
-  }
-}