Skip to content

Commit

Permalink
Add writable number entity
Browse files Browse the repository at this point in the history
  • Loading branch information
BenPru committed Oct 30, 2021
1 parent 3f2d91a commit f080133
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 27 deletions.
17 changes: 8 additions & 9 deletions custom_components/luxtronik/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

# data[device.ain] = device
# return data

# async def async_update_coordinator() -> dict[str, LuxtronikThermostat]:
# """Fetch all device data."""
# return await hass.async_add_executor_job(_update_luxtronik_devices)

# hass.data[DOMAIN][entry.entry_id][
# CONF_COORDINATOR
# ] = coordinator = DataUpdateCoordinator(
Expand Down Expand Up @@ -136,10 +136,9 @@ def setup_internal(hass, conf):
hass.data[f"{DOMAIN}_DeviceInfo_Heating"] = DeviceInfo(
identifiers={(DOMAIN, 'Heating', sn)},
default_name='Heating')
if luxtronik.get_value(LUX_SENSOR_DETECT_COOLING):
hass.data[f"{DOMAIN}_DeviceInfo_Cooling"] = DeviceInfo(
identifiers={(DOMAIN, 'Cooling', sn)},
default_name='Cooling')
hass.data[f"{DOMAIN}_DeviceInfo_Cooling"] = DeviceInfo(
identifiers={(DOMAIN, 'Cooling', sn)},
default_name='Cooling') if luxtronik.get_value(LUX_SENSOR_DETECT_COOLING) else None

def write_parameter(service):
"""Write a parameter to the Luxtronik heatpump."""
Expand Down Expand Up @@ -178,9 +177,9 @@ def get_value(self, group_sensor_id: str):

def get_sensor_by_id(self, group_sensor_id: str):
try:
group = group_sensor_id.split('.')[0]
sensor_id = group_sensor_id.split('.')[1]
return self.get_sensor(group, sensor_id)
group = group_sensor_id.split('.')[0]
sensor_id = group_sensor_id.split('.')[1]
return self.get_sensor(group, sensor_id)
except Exception as e:
LOGGER.critical(group_sensor_id, e, exc_info=True)

Expand Down
17 changes: 8 additions & 9 deletions custom_components/luxtronik/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from . import DOMAIN as LUXTRONIK_DOMAIN
from . import LuxtronikDevice
from .const import *

Expand All @@ -43,7 +42,7 @@
async def async_setup_platform(
hass: HomeAssistant, config: ConfigType, async_add_entities: AddEntitiesCallback, discovery_info: dict[str, Any] = None,
) -> None:
luxtronik: LuxtronikDevice = hass.data.get(LUXTRONIK_DOMAIN)
luxtronik: LuxtronikDevice = hass.data.get(DOMAIN)
if not luxtronik:
LOGGER.warning("climate.async_setup_platform no luxtronik!")
return False
Expand All @@ -56,7 +55,7 @@ async def async_setup_platform(
LuxtronikDomesticWaterThermostat(
hass, luxtronik, deviceInfoDomesticWater)
# , LuxtronikHeatingThermostat(hass, luxtronik, deviceInfoHeating)
]
]

if luxtronik.get_value(LUX_SENSOR_DETECT_COOLING):
deviceInfoCooling = hass.data[f"{DOMAIN}_DeviceInfo_Cooling"]
Expand Down Expand Up @@ -97,7 +96,7 @@ def __init__(self, hass: HomeAssistant, luxtronik: LuxtronikDevice, deviceInfo:
self._luxtronik = luxtronik
self._attr_device_info = deviceInfo
self.entity_id = ENTITY_ID_FORMAT.format(
f"{LUXTRONIK_DOMAIN}_{self._attr_unique_id}")
f"{DOMAIN}_{self._attr_unique_id}")

@property
def hvac_action(self):
Expand Down Expand Up @@ -171,7 +170,7 @@ async def async_set_hvac_mode(self, hvac_mode: str) -> None:
self.__get_luxmode(hvac_mode, self.preset_mode), True)

@property
def preset_mode(self): # -> str | None:
def preset_mode(self): # -> str | None:
"""Return current preset mode."""
luxmode = self._luxtronik.get_value(self._heater_sensor)
if luxmode in [LUX_MODE_OFF, LUX_MODE_AUTOMATIC]:
Expand Down Expand Up @@ -228,7 +227,7 @@ class LuxtronikDomesticWaterThermostat(LuxtronikThermostat):
_attr_unique_id: Final = 'domestic_water'
_attr_name = "Domestic Water"
_attr_icon = 'mdi:water-boiler'
_attr_device_class: Final = f"{LUXTRONIK_DOMAIN}__{_attr_unique_id}"
_attr_device_class: Final = f"{DOMAIN}__{_attr_unique_id}"

_attr_target_temperature_step = 2.5

Expand Down Expand Up @@ -276,7 +275,7 @@ class LuxtronikCoolingThermostat(LuxtronikThermostat):
# discovery_info # : dict[str, Any] | None = None,
# ) -> None:
# """Set up the Luxtronik climate sensor."""
# luxtronik: LuxtronikDevice = hass.data.get(LUXTRONIK_DOMAIN)
# luxtronik: LuxtronikDevice = hass.data.get(DOMAIN)
# _LOGGER.info("Set up the Luxtronik climate sensor.")
# if not luxtronik:
# _LOGGER.warning("climate.setup_platform no luxtronik!")
Expand Down Expand Up @@ -328,11 +327,11 @@ class LuxtronikCoolingThermostat(LuxtronikThermostat):
# hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
# ) -> None:
# """Set up the Luxtronik thermostat from ConfigEntry."""
# luxtronik = hass.data.get(LUXTRONIK_DOMAIN)
# luxtronik = hass.data.get(DOMAIN)
# if not luxtronik:
# return False

# coordinator = hass.data[LUXTRONIK_DOMAIN][entry.entry_id][CONF_COORDINATOR]
# coordinator = hass.data[DOMAIN][entry.entry_id][CONF_COORDINATOR]

# async_add_entities(
# [
Expand Down
8 changes: 4 additions & 4 deletions custom_components/luxtronik/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,12 @@ def discover(self):
# if the response starts with the magic nonsense
if res.startswith("2500;111;"):
res = res.split(";")
LOGGER.debug(f"Received answer from {ip} \"{res}\"")
LOGGER.debug(f"Received answer from {ip} \"{res}\"")
try:
port = int(res[2])
except ValueError:
LOGGER.debug("Response did not contain a valid port number, an old Luxtronic software version might be the reason.")
port = None
except ValueError:
LOGGER.debug("Response did not contain a valid port number, an old Luxtronic software version might be the reason.")
port = None
return (ip, port)
# if not, continue
else:
Expand Down
2 changes: 1 addition & 1 deletion custom_components/luxtronik/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
LOGGER: Final[logging.Logger] = logging.getLogger(__package__)

# "binary_sensor"
PLATFORMS: Final[list[str]] = ["climate", "sensor"]
PLATFORMS: Final[list[str]] = ["climate", "sensor", "number"]

PRESET_SECOND_HEATSOURCE: Final = 'second_heatsource'

Expand Down
193 changes: 193 additions & 0 deletions custom_components/luxtronik/number.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
"""Luxtronik heatpump number."""
# region Imports
import logging
import time
from threading import Timer
from typing import Any, Final, Literal

from homeassistant.components.number import NumberEntity
from homeassistant.components.sensor import (ENTITY_ID_FORMAT,
STATE_CLASS_MEASUREMENT,
SensorEntity)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (CONF_SENSORS, DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_TIMESTAMP, PRECISION_HALVES,
PRECISION_TENTHS, TEMP_CELSIUS, TIME_SECONDS)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.entity import DeviceInfo, async_generate_entity_id
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from . import LuxtronikDevice
from .const import *

# from .debounce import debounce

# from homeassistant.components.number.const import MODE_AUTO, MODE_BOX, MODE_SLIDER


# endregion Imports

# region Constants
# endregion Constants

def debounce(wait):
""" Decorator that will postpone a functions
execution until after wait seconds
have elapsed since the last time it was invoked. """

def decorator(fn):
def debounced(*args, **kwargs):
def call_it():
debounced._timer = None
debounced._last_call = time.time()
return fn(*args, **kwargs)

time_since_last_call = time.time() - debounced._last_call
if time_since_last_call >= wait:
return call_it()

if debounced._timer is None:
debounced._timer = Timer(wait - time_since_last_call, call_it)
debounced._timer.start()

debounced._timer = None
debounced._last_call = 0

return debounced

return decorator


async def async_setup_platform(
hass: HomeAssistant, config: ConfigType, async_add_entities: AddEntitiesCallback, discovery_info: dict[str, Any] = None,
) -> None:
luxtronik: LuxtronikDevice = hass.data.get(DOMAIN)
if not luxtronik:
LOGGER.warning("number.async_setup_platform no luxtronik!")
return False

deviceInfo = hass.data[f"{DOMAIN}_DeviceInfo"]
deviceInfoDomesticWater = hass.data[f"{DOMAIN}_DeviceInfo_Domestic_Water"]
deviceInfoHeating = hass.data[f"{DOMAIN}_DeviceInfo_Heating"]
entities = [
LuxtronikNumber(hass, luxtronik, deviceInfoHeating, 'parameters.ID_Einst_WK_akt',
'heating_temperature_correction', 'Temperature Correction', False,
'mdi:plus-minus-variant', DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT,
TEMP_CELSIUS, -5.0, 5.0, 0.5),
]
if luxtronik.get_value(LUX_SENSOR_DETECT_COOLING):
deviceInfoCooling = hass.data[f"{DOMAIN}_DeviceInfo_Cooling"]

async_add_entities(entities)


async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up a Luxtronik number from ConfigEntry."""
await async_setup_platform(hass, {}, async_add_entities)


class LuxtronikNumber(NumberEntity):
"""Representation of a Luxtronik number."""
_attr_should_poll = True

def __init__(
self,
hass: HomeAssistant,
luxtronik: LuxtronikDevice,
deviceInfo: DeviceInfo,
number_key: str,
unique_id: str,
name: str,
assumed: bool,
icon: str = 'mdi:thermometer',
device_class: str = DEVICE_CLASS_TEMPERATURE,
state_class: str = STATE_CLASS_MEASUREMENT,
unit_of_measurement: str = TEMP_CELSIUS,

min_value: float = None, # | None = None,
max_value: float = None, # | None = None,
step: float = None, # | None = None,
mode: Literal["auto", "box", "slider"] = "auto", # MODE_AUTO,
) -> None:
"""Initialize the number."""
self._hass = hass
self._luxtronik = luxtronik
self._number_key = number_key

self.entity_id = ENTITY_ID_FORMAT.format(f"{DOMAIN}_{unique_id}")
self._attr_unique_id = self.entity_id
self._attr_device_class = device_class
self._attr_name = name
self._attr_assumed_state = assumed
self._icon = icon
# self._attr_icon = icon
self._attr_native_unit_of_measurement = unit_of_measurement
self._attr_state_class = state_class

self._attr_device_info = deviceInfo
self._attr_mode = mode

if min_value is not None:
self._attr_min_value = min_value
if max_value is not None:
self._attr_max_value = max_value
if step is not None:
self._attr_step = step

@property
def icon(self): # -> str | None:
"""Return the icon to be used for this entity."""
# if type(self._icon) is dict and not isinstance(self._icon, str):
# return self._icon[self.native_value()]
return self._icon

# @property
# def native_value(self): # -> float | int | None:
# """Return the state of the number."""
# return self._luxtronik.get_value(self._number_key)

def update(self):
"""Get the latest status and use it to update our sensor state."""
self._luxtronik.update()

@property
def value(self) -> float:
"""Return the state of the entity."""
return self._luxtronik.get_value(self._number_key)

# @debounce(5)
async def async_set_value(self, value: float) -> None:
# def set_value(self, value: float) -> None:
"""Update the current value."""
LOGGER.info('async_set_value value: "%s"', value)

self._luxtronik.write(self._number_key.split('.')[1], value, False)

async def __async_set_value_internal():
await self.__async_set_value_internal(value)

# Debouncer(
# self._hass,
# LOGGER,
# cooldown=3,
# immediate=True,
# function=__async_set_value_internal,
# ).async_call()

async def __async_set_value_internal(self, value) -> None:
LOGGER.info('__async_set_value_internal value: "%s"', value)
# self._attr_value = value
self._luxtronik.write(self._number_key.split('.')[1], value, True)
# await self.async_write_ha_state()
LOGGER.info('__async_set_value_internal ready value: "%s"', value)

# @callback
# def _update_and_write_state(self, *_):
# """Update the number and write state."""
# self._update()
# self.async_write_ha_state()
14 changes: 10 additions & 4 deletions custom_components/luxtronik/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
# region Constants
# endregion Constants

# EVU active
# ID_WEB_EVUin


async def async_setup_platform(
hass: HomeAssistant, config: ConfigType, async_add_entities: AddEntitiesCallback, discovery_info: dict[str, Any] = None,
Expand Down Expand Up @@ -68,8 +71,7 @@ async def async_setup_platform(
LuxtronikSensor(hass, luxtronik, deviceInfoHeating, 'calculations.ID_WEB_Temperatur_TSS',
'solar_buffer_temperature', 'Solar Buffer Temperature', 'mdi:propane-tank-outline')
]
if luxtronik.get_value(LUX_SENSOR_DETECT_COOLING):
deviceInfoCooling = hass.data[f"{DOMAIN}_DeviceInfo_Cooling"]
deviceInfoCooling = hass.data[f"{DOMAIN}_DeviceInfo_Cooling"]

async_add_entities(entities)

Expand Down Expand Up @@ -105,7 +107,7 @@ def __init__(
self.entity_id = ENTITY_ID_FORMAT.format(
f"{LUXTRONIK_DOMAIN}_{unique_id}")
self._attr_unique_id = self.entity_id
# self._attr_device_class = device_class
self._attr_device_class = device_class
self._attr_name = name
self._icon = icon
# self._attr_icon = icon
Expand All @@ -126,10 +128,14 @@ def icon(self): # -> str | None:
return self._icon

@property
def native_value(self): # -> float | int | None:
def native_value(self): # -> float | int | None:
"""Return the state of the sensor."""
return self._luxtronik.get_value(self._sensor_key)

def update(self):
"""Get the latest status and use it to update our sensor state."""
self._luxtronik.update()

# @callback
# def _update_and_write_state(self, *_):
# """Update the sensor and write state."""
Expand Down

0 comments on commit f080133

Please sign in to comment.