diff --git a/custom_components/luxtronik/binary_sensor.py b/custom_components/luxtronik/binary_sensor.py index 2b7879b..dd6aef1 100644 --- a/custom_components/luxtronik/binary_sensor.py +++ b/custom_components/luxtronik/binary_sensor.py @@ -26,8 +26,8 @@ CONF_VISIBILITIES, DEFAULT_DEVICE_CLASS, DEVICE_CLASSES, DOMAIN, LOGGER, LUX_BINARY_SENSOR_ADDITIONAL_CIRCULATION_PUMP, - LUX_BINARY_SENSOR_DOMESTIC_WATER_RECIRCULATION_PUMP, LUX_BINARY_SENSOR_CIRCULATION_PUMP_HEATING, + LUX_BINARY_SENSOR_DOMESTIC_WATER_RECIRCULATION_PUMP, LUX_BINARY_SENSOR_EVU_UNLOCKED, LUX_BINARY_SENSOR_SOLAR_PUMP) from .helpers.helper import get_sensor_text @@ -253,7 +253,6 @@ async def async_setup_entry( deviceInfoDomesticWater = hass.data[f"{DOMAIN}_DeviceInfo_Domestic_Water"] if deviceInfoDomesticWater is not None: - text_solar_pump = get_sensor_text(lang, "solar_pump") if luxtronik.has_domestic_water_circulation_pump: circulation_pump_unique_id = 'domestic_water_circulation_pump' text_domestic_water_circulation_pump = text_circulation_pump @@ -282,6 +281,7 @@ async def async_setup_entry( ] solar_present = luxtronik.detect_solar_present() if solar_present: + text_solar_pump = get_sensor_text(lang, "solar_pump") entities += [ LuxtronikBinarySensor( luxtronik=luxtronik, diff --git a/custom_components/luxtronik/const.py b/custom_components/luxtronik/const.py index cf3e6bc..40d1cd0 100644 --- a/custom_components/luxtronik/const.py +++ b/custom_components/luxtronik/const.py @@ -250,7 +250,7 @@ class LuxMode(Enum): ] # endregion Luxtronik Sensor ids -LUX_DETECT_SOLAR_SENSOR: Final = "parameters.ID_BSTD_Solar" +LUX_DETECT_SOLAR_SENSOR: Final = "visibilities.ID_Visi_Solar" LUX_MK_SENSORS = ['parameters.ID_Einst_MK1Typ_akt', 'parameters.ID_Einst_MK2Typ_akt', 'parameters.ID_Einst_MK3Typ_akt'] diff --git a/custom_components/luxtronik/diagnostics.py b/custom_components/luxtronik/diagnostics.py new file mode 100644 index 0000000..2554fd2 --- /dev/null +++ b/custom_components/luxtronik/diagnostics.py @@ -0,0 +1,84 @@ +"""Diagnostics support for Luxtronik.""" +from __future__ import annotations + +from functools import partial +from ipaddress import IPv6Address, ip_address + +from async_timeout import timeout +from getmac import get_mac_address +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import (CONF_HOST, CONF_PASSWORD, CONF_PORT, + CONF_USERNAME) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry +from luxtronik import Luxtronik as Lux + +from .const import CONF_COORDINATOR, DOMAIN + +TO_REDACT = {CONF_USERNAME, CONF_PASSWORD} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict: + """Return diagnostics for a config entry.""" + data: dict = entry.data + client = Lux(data[CONF_HOST], data[CONF_PORT], True) + client.read() + + mac = "" + async with timeout(10): + mac = await _async_get_mac_address(hass, data[CONF_HOST]) + mac = mac[:9] + '*' + + entry_data = async_redact_data(entry.as_dict(), TO_REDACT) + if "data" not in entry_data: + entry_data["data"] = {} + entry_data["data"]["mac"] = mac + diag_data = { + "entry": entry_data, + "parameters": _dump_items(client.parameters.parameters), + "calculations": _dump_items(client.calculations.calculations), + "visibilities": _dump_items(client.visibilities.visibilities), + } + return diag_data + + +def _dump_items(items: dict) -> dict: + dump = dict() + for index, item in items.items(): + dump[f"{index:<4d} {item.name:<60}"] = f"{items.get(index)}" + return dump + + +async def _async_get_mac_address(hass: HomeAssistant, host: str) -> str | None: + """Get mac address from host name, IPv4 address, or IPv6 address.""" + # Help mypy, which has trouble with the async_add_executor_job + partial call + mac_address: str | None + # getmac has trouble using IPv6 addresses as the "hostname" parameter so + # assume host is an IP address, then handle the case it's not. + try: + ip_addr = ip_address(host) + except ValueError: + mac_address = await hass.async_add_executor_job( + partial(get_mac_address, hostname=host) + ) + else: + if ip_addr.version == 4: + mac_address = await hass.async_add_executor_job( + partial(get_mac_address, ip=host) + ) + else: + # Drop scope_id from IPv6 address by converting via int + ip_addr = IPv6Address(int(ip_addr)) + mac_address = await hass.async_add_executor_job( + partial(get_mac_address, ip6=str(ip_addr)) + ) + + if not mac_address: + return None + + return device_registry.format_mac(mac_address) + + diff --git a/custom_components/luxtronik/luxtronik_device.py b/custom_components/luxtronik/luxtronik_device.py index ac31574..1b8987d 100644 --- a/custom_components/luxtronik/luxtronik_device.py +++ b/custom_components/luxtronik/luxtronik_device.py @@ -107,7 +107,10 @@ def detect_cooling_Mk(self): def detect_solar_present(self): sensor_value = self.get_value(LUX_DETECT_SOLAR_SENSOR) - SolarPresent = (sensor_value > 0.01) + solar_koll = self.get_value("visibilities.ID_Visi_Temp_Solarkoll") + solar_buffer = self.get_value("visibilities.ID_Visi_Temp_Solarsp") + working_hours = self.get_value("parameters.ID_BSTD_Solar") + SolarPresent = (sensor_value > 0 or working_hours > 0.01 or solar_koll > 0 or solar_buffer > 0) LOGGER.info(f"SolarPresent = {SolarPresent}") return SolarPresent diff --git a/custom_components/luxtronik/manifest.json b/custom_components/luxtronik/manifest.json index 4f25b13..2aef6d3 100644 --- a/custom_components/luxtronik/manifest.json +++ b/custom_components/luxtronik/manifest.json @@ -1,7 +1,7 @@ { "domain": "luxtronik2", "name": "Luxtronik", - "version": "2022.12.30", + "version": "2023.01.05", "integration_type": "hub", "config_flow": true, "iot_class": "local_polling", @@ -10,7 +10,7 @@ "dependencies": [], "after_dependencies": ["http"], "codeowners": ["@bouni", "@benpru"], - "requirements": ["luxtronik>=0.3.14"], + "requirements": ["luxtronik>=0.3.14", "getmac>=0.8.2"], "homeassistant": "2022.9.0", "dhcp": [ { diff --git a/custom_components/luxtronik/recorder.py b/custom_components/luxtronik/recorder.py new file mode 100644 index 0000000..5f709a2 --- /dev/null +++ b/custom_components/luxtronik/recorder.py @@ -0,0 +1,37 @@ +"""Integration platform for recorder.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant, callback + +from .const import (ATTR_EXTRA_STATE_ATTRIBUTE_LAST_THERMAL_DESINFECTION, + ATTR_STATUS_TEXT, DOMAIN) + + +@callback +def exclude_attributes(hass: HomeAssistant) -> set[str]: + """Exclude attributes from being recorded in the database.""" + return { + ATTR_STATUS_TEXT, + ATTR_EXTRA_STATE_ATTRIBUTE_LAST_THERMAL_DESINFECTION, + 'WP Seit (ID_WEB_Time_WPein_akt)', + 'ZWE1 seit (ID_WEB_Time_ZWE1_akt)', + 'ZWE2 seit (ID_WEB_Time_ZWE2_akt)', + 'Netzeinschaltv. (ID_WEB_Timer_EinschVerz)', + 'Schaltspielsperre SSP-Aus-Zeit (ID_WEB_Time_SSPAUS_akt)', + 'Schaltspielsperre SSP-Ein-Zeit (ID_WEB_Time_SSPEIN_akt)', + 'VD-Stand (ID_WEB_Time_VDStd_akt)', + 'Heizungsregler Mehr-Zeit HRM-Zeit (ID_WEB_Time_HRM_akt)', + 'Heizungsregler Weniger-Zeit HRW-Stand (ID_WEB_Time_HRW_akt)', + 'ID_WEB_Time_LGS_akt', + 'Sperre WW? ID_WEB_Time_SBW_akt', + 'Abtauen in ID_WEB_Time_AbtIn', + 'ID_WEB_Time_Heissgas', + 'switch_gap', + f"sensor.{DOMAIN}_status_time", + 'status raw', + 'EVU first start time', + 'EVU first end time', + 'EVU second start time', + 'EVU second end time', + 'EVU minutes until next event', + } diff --git a/custom_components/luxtronik/sensor.py b/custom_components/luxtronik/sensor.py index 7fa0387..6c43f78 100644 --- a/custom_components/luxtronik/sensor.py +++ b/custom_components/luxtronik/sensor.py @@ -1,7 +1,7 @@ """Luxtronik heatpump sensor.""" # region Imports -from typing import Any from datetime import datetime, time +from typing import Any from homeassistant.components.sensor import (ENTITY_ID_FORMAT, STATE_CLASS_MEASUREMENT, @@ -588,7 +588,6 @@ async def async_setup_entry( text_operation_hours_domestic_water = get_sensor_text( lang, "operation_hours_domestic_water" ) - text_operation_hours_solar = get_sensor_text(lang, "operation_hours_solar") text_heat_amount_domestic_water = get_sensor_text( lang, "heat_amount_domestic_water" ) @@ -630,8 +629,22 @@ async def async_setup_entry( ), ] - solar_present = luxtronik.detect_solar_present() - if solar_present: + if luxtronik.get_value("visibilities.ID_Visi_Temp_Solarkoll") > 0: + entities += [ + LuxtronikSensor( + luxtronik, + device_info_domestic_water, + "calculations.ID_WEB_Temperatur_TSK", + "solar_collector_temperature", + f"Solar {text_collector}", + "mdi:solar-panel-large", + entity_category=None, + ), + ] + # Temp. disabled: + # solar_present = luxtronik.detect_solar_present() + # if solar_present: + if luxtronik.get_value("visibilities.ID_Visi_Temp_Solarkoll") > 0: entities += [ LuxtronikSensor( luxtronik, @@ -642,6 +655,9 @@ async def async_setup_entry( "mdi:solar-panel-large", entity_category=None, ), + ] + if luxtronik.get_value("visibilities.ID_Visi_Temp_Solarsp") > 0: + entities += [ LuxtronikSensor( luxtronik, device_info_domestic_water, @@ -651,6 +667,10 @@ async def async_setup_entry( "mdi:propane-tank-outline", entity_category=None, ), + ] + if luxtronik.get_value("parameters.ID_BSTD_Solar") > 0: + text_operation_hours_solar = get_sensor_text(lang, "operation_hours_solar") + entities += [ LuxtronikSensor( luxtronik, device_info_domestic_water, @@ -664,7 +684,6 @@ async def async_setup_entry( entity_category=EntityCategory.DIAGNOSTIC, factor=SECOUND_TO_HOUR_FACTOR, ), - ] deviceInfoCooling = hass.data[f"{DOMAIN}_DeviceInfo_Cooling"]