From 06455543443d0afdd423852209a50d7e78dcf2ed Mon Sep 17 00:00:00 2001 From: hsk-dk Date: Sun, 16 Feb 2025 10:14:35 +0100 Subject: [PATCH 1/9] Update __init__.py From 0b1609b1278e9af4d558f6f2708363028161dcac Mon Sep 17 00:00:00 2001 From: hsk-dk Date: Sun, 16 Feb 2025 10:16:19 +0100 Subject: [PATCH 2/9] Update api.py commit from local --- thermex_api/api.py | 75 +++++++++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 38 deletions(-) diff --git a/thermex_api/api.py b/thermex_api/api.py index 9d3c9c0..bce3d58 100644 --- a/thermex_api/api.py +++ b/thermex_api/api.py @@ -1,4 +1,3 @@ -"""Thermex API module""" import aiohttp import json import logging @@ -16,6 +15,22 @@ def __init__(self, host, code): self._password = code self._coordinator = None + @property + def coordinator(self): + """Return the coordinator.""" + return self._coordinator + + async def async_setup_coordinator(self): + """Set up data update coordinator.""" + self._coordinator = DataUpdateCoordinator( + self._host, + _LOGGER, + name="thermex_data", + update_method=self.async_get_data, + update_interval=timedelta(seconds=10), + ) + await self._coordinator.async_refresh() + async def authenticate(self, websocket): auth_message = { "Request": "Authenticate", @@ -31,6 +46,26 @@ async def authenticate(self, websocket): else: _LOGGER.error("Authentication failed") return False + + async def fetch_status(self): + """Fetch status of fan and light.""" + async with aiohttp.ClientSession() as session: + async with session.ws_connect(f'ws://{self._host}:9999/api') as websocket: + await self.authenticate(websocket) + _LOGGER.debug("Fetching fan and light status") + await websocket.send_json({"Request": "STATUS"}) + response = await websocket.receive() + response = json.loads(response.data) + _LOGGER.debug("Status response: %s", response) + if response.get("Response") == "Status": + return response.get("Data") + else: + _LOGGER.error("Error fetching status: %s", response) + raise Exception("Unexpected response from Thermex API") + + async def async_get_data(self): + """Fetch and return status data.""" + return await self.fetch_status() async def get_fan_status(self): """Get the status of the fan.""" @@ -49,7 +84,7 @@ async def get_fan_status(self): raise Exception("api.py Uventet svar fra Thermex API") else: _LOGGER.error("api.py Fejl under hentning af fan status") - + async def update_fan(self, fanonoff, fanspeed): """Update fan settings.""" async with aiohttp.ClientSession() as session: @@ -71,42 +106,6 @@ async def update_fan(self, fanonoff, fanspeed): else: _LOGGER.error("Update failed due to authentication failure") - async def fetch_status(self): - """Fetch status of fan and light.""" - async with aiohttp.ClientSession() as session: - async with session.ws_connect(f'ws://{self._host}:9999/api') as websocket: - await self.authenticate(websocket) - _LOGGER.debug("Fetching fan and light status") - await websocket.send_json({"Request": "STATUS"}) - response = await websocket.receive() - response = json.loads(response.data) - _LOGGER.debug("Status response: %s", response) - if response.get("Response") == "Status": - return response.get("Data") - else: - _LOGGER.error("Error fetching status: %s", response) - raise Exception("Unexpected response from Thermex API") - - async def async_get_data(self): - """Fetch and return status data.""" - return await self.fetch_status() - - async def async_setup_coordinator(self): - """Set up data update coordinator.""" - self._coordinator = DataUpdateCoordinator( - self._host, - _LOGGER, - name="thermex_data", - update_method=self.async_get_data, - update_interval=timedelta(seconds=10), - ) - await self._coordinator.async_refresh() - - @property - def coordinator(self): - """Return the coordinator.""" - return self._coordinator - async def update_light(self, lightonoff, brightness=None): """Update light settings.""" _LOGGER.debug("update_light(%s)", lightonoff) From 8ab733d924ac35e4672384b3810dd25163dcce63 Mon Sep 17 00:00:00 2001 From: hsk-dk Date: Sun, 16 Feb 2025 10:18:32 +0100 Subject: [PATCH 3/9] Update light.py commit from local --- thermex_api/light.py | 90 ++++++++++++++++++++++++++++---------------- 1 file changed, 57 insertions(+), 33 deletions(-) diff --git a/thermex_api/light.py b/thermex_api/light.py index 17799ac..c55fcf3 100644 --- a/thermex_api/light.py +++ b/thermex_api/light.py @@ -1,5 +1,5 @@ import logging -#from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import Entity from homeassistant.components.light import ColorMode, LightEntity, LightEntityFeature from .const import DOMAIN @@ -18,13 +18,59 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class ThermexLight(LightEntity): """Representation of a light entity.""" + _attr_color_mode = ColorMode.BRIGHTNESS + _attr_supported_color_modes = {ColorMode.BRIGHTNESS} def __init__(self, coordinator, name="Thermex Light"): """Initialize the light entity.""" self._coordinator = coordinator - self._name = name + self._attr_name = name self._state = None - self._brightness = None + self._attr_brightness = None + self._attr_unique_id = "fsfdsfsdfsdf3r" + + @property + def unique_id(self): + """Return the name of the light.""" + return self._attr_unique_id + + @property + def name(self): + """Return the name of the light.""" + return self._attr_name + + @property + def supported_color_modes(self): + """Set of supported color modes.""" + return self._attr_supported_color_modes + + @property + def color_mode(self): + """Current color mode of the light.""" + if self._attr_state == True: + if self._attr_brightness is not None: + self._attr_color_mode = ColorMode.BRIGHTNESS + self._attr_color_mode = ColorMode.ONOFF + self._attr_color_mode = ColorMode.UNKNOWN + return self._attr_color_mode + + @property + def is_on(self): + """Return true if the light is on.""" + return self._attr_state + + @property + def brightness(self): + """Return the brightness of the light.""" + return self._attr_brightness + + @property + def icon(self) -> str | None: + """Icon based on state.""" + if self._attr_state: + return "mdi:pot-steam" + else: + return "mdi:pot-steam-outline" async def async_update(self): """Fetch the latest data from the coordinator.""" @@ -36,37 +82,20 @@ async def async_update(self): _LOGGER.debug("Light on/off status: %s", lightonoff) if lightonoff == 1: - self._state = True + self._attr_state = True elif lightonoff == 0: - self._state = False + self._attr_state = False else: _LOGGER.warning("Unexpected value for lightonoff: %s", lightonoff) - self._state = None + self._attr_state = None _LOGGER.debug("Light status sat: %s", self._state) self._brightness = light_data.get("lightbrightness") - - @property - def supported_color_modes(self): - """Set of supported color modes.""" - return {ColorMode.ONOFF, ColorMode.BRIGHTNESS} - - @property - def color_mode(self): - """Current color mode of the light.""" - if self._brightness is not None: - return ColorMode.BRIGHTNESS - return ColorMode.ONOFF - - @property - def name(self): - """Return the name of the light.""" - return self._name - - @property - def is_on(self): - """Return true if the light is on.""" - return self._state + if self._attr_state == True: + if self._attr_brightness is not None: + self._attr_color_mode = ColorMode.BRIGHTNESS + self._attr_color_mode = ColorMode.ONOFF + self._attr_color_mode = ColorMode.UNKNOWN async def async_turn_on(self, **kwargs): """Turn on the light.""" @@ -76,8 +105,3 @@ async def async_turn_on(self, **kwargs): async def async_turn_off(self, **kwargs): """Turn off the light.""" await self._coordinator.update_light(lightonoff=0) - - @property - def brightness(self): - """Return the brightness of the light.""" - return self._brightness From 0e1358297cf11888d04271ee9d830588cff2e7f7 Mon Sep 17 00:00:00 2001 From: hsk-dk Date: Sun, 16 Feb 2025 10:19:20 +0100 Subject: [PATCH 4/9] Update manifest.json commit from local --- thermex_api/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/thermex_api/manifest.json b/thermex_api/manifest.json index 06f4ae9..a195fe0 100644 --- a/thermex_api/manifest.json +++ b/thermex_api/manifest.json @@ -6,6 +6,6 @@ "codeowners": ["@hsk-dk"], "config_flow": false, "requirements": [], - "version": "0.0.1", + "version": "0.0.2", "iot_class": "local_pool" } From 93b960d826d00ad879f80bc67fe691c2f69b58da Mon Sep 17 00:00:00 2001 From: hsk-dk Date: Sun, 16 Feb 2025 10:19:59 +0100 Subject: [PATCH 5/9] Update sensor.py commit from local --- thermex_api/sensor.py | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/thermex_api/sensor.py b/thermex_api/sensor.py index 108bbf9..70fa7d0 100644 --- a/thermex_api/sensor.py +++ b/thermex_api/sensor.py @@ -19,7 +19,6 @@ class ThermexFanSensor(Entity): def __init__(self, coordinator): """Initialize the sensor.""" - #self._api = api self._state = None self._coordinator = coordinator self._speeds = { @@ -29,14 +28,15 @@ def __init__(self, coordinator): 3: "høj", 4: "boost" } + self._attr_unique_id = "fdddsfdsfsdfsdf3r" + _LOGGER.debug("Sensor.py Thermex ThermexFanSensor initialiseret") - async def async_update(self): - """Fetch the latest data from the coordinator.""" - data = await self._coordinator.async_get_data() - fan_data = data.get("Fan", {}) - self._state = self._speeds.get(fan_data.get("fanspeed", 0), "ukendt") - + @property + def unique_id(self): + """Return the name of the light.""" + return self._attr_unique_id + @property def name(self): """Return the name of the sensor.""" @@ -51,3 +51,24 @@ def state(self): def unit_of_measurement(self): """Return the unit of measurement.""" return None + + @property + def icon(self) -> str | None: + """Icon based on state.""" + if self._attr_state == "Lav": + return "mdi:fan-speed-1" + elif self._attr_state == "Mellem": + return "mdi:fan-speed-2" + elif self._attr_state == "Høj": + return "mdi:fan-speed-3" + elif self._attr_state == "Boost": + return "mdi:fan-plus" + else: + return "mdi:fan-off" + + async def async_update(self): + """Fetch the latest data from the coordinator.""" + data = await self._coordinator.async_get_data() + fan_data = data.get("Fan", {}) + self._state = self._speeds.get(fan_data.get("fanspeed", 0), "ukendt") + From 23bec5a0bdae87faa6d9fe891e4836ff005bc818 Mon Sep 17 00:00:00 2001 From: hsk-dk Date: Sun, 16 Feb 2025 10:20:38 +0100 Subject: [PATCH 6/9] Update services.yaml commit from local From 51eb95e4b79fe5a2873e5e35c32377d267c302d6 Mon Sep 17 00:00:00 2001 From: hsk-dk Date: Sun, 16 Feb 2025 10:21:21 +0100 Subject: [PATCH 7/9] Create switch.py Commit from local --- thermex_api/switch.py | 69 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 thermex_api/switch.py diff --git a/thermex_api/switch.py b/thermex_api/switch.py new file mode 100644 index 0000000..eb66941 --- /dev/null +++ b/thermex_api/switch.py @@ -0,0 +1,69 @@ +"""Platform for a generic switch.""" +import logging + +from homeassistant.components.switch import SwitchEntity + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Generic Switch platform.""" + # Hent API fra hass.data + api = hass.data[DOMAIN] + # Replace 'your_device_id' and 'Your Switch' with appropriate values + async_add_entities([ThermexFanSwitch(api)], True) + + +class ThermexFanSwitch(SwitchEntity): + """Representation of a Generic Switch.""" + + def __init__(self, coordinator, name="Thermex Fan Switch"): + """Initialize the switch.""" + self._coordinator = coordinator + self._attr_id = "Fan_Switch"#device_id + self._attr_name = name + self._attr_state = False + + @property + def unique_id(self): + """Return a unique ID to use for this switch.""" + return f"{DOMAIN}_{self._attr_id}" + + @property + def name(self): + """Return the display name of this switch.""" + return self._attr_name + + @property + def is_on(self): + """Return true if the switch is on.""" + return self._attr_state + + async def async_turn_on(self, **kwargs): + """Turn the switch on.""" + _LOGGER.info("Turning on the switch") + # Add code to turn on the switch + await self._coordinator.update_fan(fanonoff=1, fanspeed=2) + self._attr_state = True + + async def async_turn_off(self, **kwargs): + """Turn the switch off.""" + _LOGGER.info("Turning off the switch") + # Add code to turn off the switch + await self._coordinator.update_fan(fanonoff=0, fanspeed=2) + self._attr_state = False + + async def async_update(self): + """Update the switch's state.""" + _LOGGER.debug("Updating switch state") + data = await self._coordinator.async_get_data() + fan_data = data.get("Fan", {}) + fanonoff = fan_data.get("fanonoff", 0) + if fanonoff == 1: + self._attr_state = True + elif fanonoff == 0: + self._attr_state = False + else: + _LOGGER.warning("Unexpected value for fanonoff: %s", fanonoff) + self._attr_state = None From 5ef97535fe0e676ccb4ef27d5ba7ee0fb64cbfa5 Mon Sep 17 00:00:00 2001 From: hsk-dk Date: Sun, 16 Feb 2025 10:21:48 +0100 Subject: [PATCH 8/9] Add files via upload commit from local --- thermex_api/translations/da-dk.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 thermex_api/translations/da-dk.json diff --git a/thermex_api/translations/da-dk.json b/thermex_api/translations/da-dk.json new file mode 100644 index 0000000..259c414 --- /dev/null +++ b/thermex_api/translations/da-dk.json @@ -0,0 +1,16 @@ +{ + "config": + { + "fields": + { + "host": + { "name": "IP", "description": "IP Adresse til Thermex emhætte" }, + "password": + { + "name": "Adgangskode", + "description": "Adgangskode til Thermex API", + "hide": true + } + } + } +} From 49c5fe88bbf3eaf031331216abafb2fe0a4a6ed5 Mon Sep 17 00:00:00 2001 From: hsk-dk Date: Sun, 16 Feb 2025 10:22:52 +0100 Subject: [PATCH 9/9] Add files via upload commit from local --- thermex_api/config_flow.py | 49 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 thermex_api/config_flow.py diff --git a/thermex_api/config_flow.py b/thermex_api/config_flow.py new file mode 100644 index 0000000..16b1676 --- /dev/null +++ b/thermex_api/config_flow.py @@ -0,0 +1,49 @@ +"""Config flow for Thermex_fan integration.""" +import logging +import voluptuous as vol +from homeassistant import config_entries +from homeassistant.loader import Integration +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema( + { + vol.Required("host", description={"suggested_value": "example.com"}): str, + vol.Required("password", description={"suggested_value": ""}): str, + } +) + +class ThermexConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + async def async_step_user(self, user_input=None): + errors = {} + + if user_input is not None: + host = user_input['host'] + password = user_input['password'] + + # Your authentication logic goes here + + # If authentication is successful + unique_id = f"thermex_{host}" + name = "Thermex" + + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + + self.context["title_placeholders"] = {"unique_id": unique_id} + + # Create entry with device name and data + return self.async_create_entry(title=name, data=user_input) + + return self.async_show_form( + step_id="user", + data_schema=DATA_SCHEMA, + errors=errors, + description_placeholders={ + "title": await self.hass.components.frontend.async_get_translations(self.hass, "config_flow", "title"), + "description": await self.hass.components.frontend.async_get_translations(self.hass, "config_flow", "description"), + "host_label": await self.hass.components.frontend.async_get_translations(self.hass, "config_flow", "host_label"), + "password_label": await self.hass.components.frontend.async_get_translations(self.hass, "config_flow", "password_label"), + } + )