diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 5a8b8758..67762806 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -5,17 +5,10 @@ labels: new bug assignees: kartoffeltoby --- -### Prerequisites - -* [ ] Model name of your Devices -* [ ] Output from Home Assistant Developer Tools state e.g. -* [ ] Output from Home Assistant Device Diagnostic from BT - -```json -{ - YOUR DEVICE DIAGNOSTICS JSON OUTPUT HERE -} -``` + ### Description @@ -35,9 +28,50 @@ assignees: kartoffeltoby -### Versions +### Versions and HW + + +Home Assistant: +Better Thermostat: + +TRV(s): + +### Debug data + +**diagnostic data** + + +```json +{ + YOUR DEVICE DIAGNOSTICS JSON OUTPUT HERE +} +``` + +**debug log** + + + - +**graphs** + ### Additional Information diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index daffe9b7..a72ce770 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -5,7 +5,7 @@ ## Related issue (check one): - [ ] fixes # -- [ ] there is no related issue ticket +- [ ] There is no related issue ticket ## Checklist (check one): @@ -14,7 +14,7 @@ ## Test-Hardware list (for code changes) - + HA Version: Zigbee2MQTT Version: @@ -22,9 +22,9 @@ TRV Hardware: ## New device mappings - + - [ ] I avoided any changes to other device mappings - [ ] There are no changes in `climate.py` - + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 633e94bc..abe633e3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,16 +23,16 @@ document in a pull request. #### Nice to know -- Debuging is possible with the VSCode Debuger. Just run the HomeAssistant in Debugger and open browser on http://localhost:9123 (No task run needed) +- Debugging is possible with the VSCode Debugger. Just run the HomeAssistant in Debugger and open your browser to http://localhost:9123 (No task run needed) - Update your local in devcontainer configuration.yaml to the current version of the repository to get the latest changes. -> Run "Sync configuration.yaml (Override local)" in Task Runner -- Test BT in a specific HA version -> Run "Install a specific version of Home Assistant" in Task Runner and the the version you want to test in the terminal promt. -- Test BT with the latest HA version -> Run "pgrade Home Assistant to latest dev" in Task Runner +- Test BT in a specific HA version -> Run "Install a specific version of Home Assistant" in Task Runner and the version you want to test in the terminal prompt. +- Test BT with the latest HA version -> Run "upgrade Home Assistant to latest dev" in Task Runner ## How Can I Contribute? ## New Adapter -If you want to add a new adapter, please create a new python file with the name of the adapter in the adapters folder. The file should contain all functions find in the generic.py. The if you adapter needs a special handling for one of the base functions, override it, if you can use generic functions, use them like: +If you want to add a new adapter, please create a new Python file with the name of the adapter in the adapters folder. The file should contain all functions found in the generic.py. If your adapter needs special handling for one of the base functions, override it, if you can use generic functions, use them like: ```python async def set_temperature(self, entity_id, temperature): @@ -55,4 +55,4 @@ https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_numpy.html ## Setup -Install the pip install pre-commit used for pre-commit hooks. \ No newline at end of file +Install the pip install pre-commit used for pre-commit hooks. diff --git a/custom_components/better_thermostat/__init__.py b/custom_components/better_thermostat/__init__.py index 6584d5a6..970cbf2e 100644 --- a/custom_components/better_thermostat/__init__.py +++ b/custom_components/better_thermostat/__init__.py @@ -25,14 +25,17 @@ config_entry_update_listener_lock = Lock() -async def async_setup(hass: HomeAssistant, config: ConfigType): - """Set up this integration using YAML is not supported.""" - hass.data[DOMAIN] = {} +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up this integration using YAML.""" + if DOMAIN in config: + hass.data.setdefault(DOMAIN, {}) return True async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - hass.data[DOMAIN] = {} + """Set up entry.""" + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = {} await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) return True @@ -44,9 +47,11 @@ async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) await hass.config_entries.async_reload(entry.entry_id) -async def async_unload_entry(hass, entry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/custom_components/better_thermostat/adapters/delegate.py b/custom_components/better_thermostat/adapters/delegate.py index 4641c58d..11fe5a84 100644 --- a/custom_components/better_thermostat/adapters/delegate.py +++ b/custom_components/better_thermostat/adapters/delegate.py @@ -7,30 +7,28 @@ async def load_adapter(self, integration, entity_id, get_name=False): """Load adapter.""" if get_name: - self.name = "-" + self.device_name = "-" if integration == "generic_thermostat": integration = "generic" try: self.adapter = await async_import_module( - self.hass, - "custom_components.better_thermostat.adapters." + integration, + self.hass, "custom_components.better_thermostat.adapters." + integration ) _LOGGER.debug( "better_thermostat %s: uses adapter %s for trv %s", - self.name, + self.device_name, integration, entity_id, ) except Exception: self.adapter = await async_import_module( - self.hass, - "custom_components.better_thermostat.adapters.generic", + self.hass, "custom_components.better_thermostat.adapters.generic" ) _LOGGER.info( "better_thermostat %s: integration: %s isn't native supported, feel free to open an issue, fallback adapter %s", - self.name, + self.device_name, integration, "generic", ) diff --git a/custom_components/better_thermostat/adapters/generic.py b/custom_components/better_thermostat/adapters/generic.py index 94a071c3..0e738042 100644 --- a/custom_components/better_thermostat/adapters/generic.py +++ b/custom_components/better_thermostat/adapters/generic.py @@ -28,7 +28,7 @@ async def init(self, entity_id): ) _LOGGER.debug( "better_thermostat %s: uses local calibration entity %s", - self.name, + self.device_name, self.real_trvs[entity_id]["local_temperature_calibration_entity"], ) # Wait for the entity to be available @@ -39,7 +39,7 @@ async def init(self, entity_id): ).state in (STATE_UNAVAILABLE, STATE_UNKNOWN, None): _LOGGER.info( "better_thermostat %s: waiting for TRV/climate entity with id '%s' to become fully available...", - self.name, + self.device_name, self.real_trvs[entity_id]["local_temperature_calibration_entity"], ) await asyncio.sleep(5) @@ -119,7 +119,7 @@ async def set_temperature(self, entity_id, temperature): async def set_hvac_mode(self, entity_id, hvac_mode): """Set new target hvac mode.""" - _LOGGER.debug("better_thermostat %s: set_hvac_mode %s", self.name, hvac_mode) + _LOGGER.debug("better_thermostat %s: set_hvac_mode %s", self.device_name, hvac_mode) try: await self.hass.services.async_call( "climate", diff --git a/custom_components/better_thermostat/adapters/mqtt.py b/custom_components/better_thermostat/adapters/mqtt.py index 288c487b..8544f7b8 100644 --- a/custom_components/better_thermostat/adapters/mqtt.py +++ b/custom_components/better_thermostat/adapters/mqtt.py @@ -36,7 +36,7 @@ async def init(self, entity_id): ) _LOGGER.debug( "better_thermostat %s: uses local calibration entity %s", - self.name, + self.device_name, self.real_trvs[entity_id]["local_temperature_calibration_entity"], ) # Wait for the entity to be available @@ -47,7 +47,7 @@ async def init(self, entity_id): ).state in (STATE_UNAVAILABLE, STATE_UNKNOWN, None): _LOGGER.info( "better_thermostat %s: waiting for TRV/climate entity with id '%s' to become fully available...", - self.name, + self.device_name, self.real_trvs[entity_id]["local_temperature_calibration_entity"], ) await asyncio.sleep(5) @@ -159,7 +159,7 @@ async def set_offset(self, entity_id, offset): async def set_valve(self, entity_id, valve): """Set new target valve.""" _LOGGER.debug( - f"better_thermostat {self.name}: TO TRV {entity_id} set_valve: {valve}" + f"better_thermostat {self.device_name}: TO TRV {entity_id} set_valve: {valve}" ) await self.hass.services.async_call( "number", diff --git a/custom_components/better_thermostat/calibration.py b/custom_components/better_thermostat/calibration.py index 4cae0e52..2c5d682c 100644 --- a/custom_components/better_thermostat/calibration.py +++ b/custom_components/better_thermostat/calibration.py @@ -1,7 +1,6 @@ """Helper functions for the Better Thermostat component.""" import logging -from typing import Union from homeassistant.components.climate.const import HVACAction @@ -12,7 +11,6 @@ from custom_components.better_thermostat.utils.helpers import ( convert_to_float, - round_down_to_half_degree, round_by_steps, heating_power_valve_position, ) @@ -25,7 +23,7 @@ _LOGGER = logging.getLogger(__name__) -def calculate_calibration_local(self, entity_id) -> Union[float, None]: +def calculate_calibration_local(self, entity_id) -> float | None: """Calculate local delta to adjust the setpoint of the TRV based on the air temperature of the external sensor. This calibration is for devices with local calibration option, it syncs the current temperature of the TRV to the target temperature of @@ -43,19 +41,29 @@ def calculate_calibration_local(self, entity_id) -> Union[float, None]: """ _context = "_calculate_calibration_local()" + def _convert_to_float(value): + return convert_to_float(value, self.name, _context) + if None in (self.cur_temp, self.bt_target_temp): return None + # Add tolerance check + _within_tolerance = self.cur_temp >= ( + self.bt_target_temp - self.tolerance + ) and self.cur_temp <= (self.bt_target_temp + self.tolerance) + + if _within_tolerance: + # When within tolerance, don't adjust calibration + return self.real_trvs[entity_id]["last_calibration"] + _cur_trv_temp_s = self.real_trvs[entity_id]["current_temperature"] _calibration_steps = self.real_trvs[entity_id]["local_calibration_steps"] _cur_external_temp = self.cur_temp _cur_target_temp = self.bt_target_temp - _cur_trv_temp_f = convert_to_float(str(_cur_trv_temp_s), self.name, _context) + _cur_trv_temp_f = _convert_to_float(_cur_trv_temp_s) - _current_trv_calibration = convert_to_float( - str(self.real_trvs[entity_id]["last_calibration"]), self.name, _context - ) + _current_trv_calibration = _convert_to_float(self.real_trvs[entity_id]["last_calibration"]) if None in ( _current_trv_calibration, @@ -64,7 +72,7 @@ def calculate_calibration_local(self, entity_id) -> Union[float, None]: _calibration_steps, ): _LOGGER.warning( - f"better thermostat {self.name}: {entity_id} Could not calculate local calibration in {_context}:" + f"better thermostat {self.device_name}: {entity_id} Could not calculate local calibration in {_context}:" f" trv_calibration: {_current_trv_calibration}, trv_temp: {_cur_trv_temp_f}, external_temp: {_cur_external_temp}" f" calibration_steps: {_calibration_steps}" ) @@ -102,24 +110,29 @@ def calculate_calibration_local(self, entity_id) -> Union[float, None]: CONF_PROTECT_OVERHEATING, False ) + # Base calibration adjustment considering tolerance + if _cur_external_temp >= _cur_target_temp + self.tolerance: + _new_trv_calibration += ( + _cur_external_temp - (_cur_target_temp + self.tolerance) + ) * 2.0 + + # Additional adjustment if overheating protection is enabled if _overheating_protection is True: - if _cur_external_temp >= _cur_target_temp: - _new_trv_calibration += (_cur_external_temp - _cur_target_temp) * 10.0 + if _cur_external_temp >= _cur_target_temp + self.tolerance: + _new_trv_calibration += ( + _cur_external_temp - (_cur_target_temp + self.tolerance) + ) * 8.0 # Reduced from 10.0 since we already add 2.0 # Adjust based on the steps allowed by the local calibration entity _new_trv_calibration = round_by_steps(_new_trv_calibration, _calibration_steps) - # Compare against min/max - if _new_trv_calibration > float(self.real_trvs[entity_id]["local_calibration_max"]): - _new_trv_calibration = float(self.real_trvs[entity_id]["local_calibration_max"]) - elif _new_trv_calibration < float( - self.real_trvs[entity_id]["local_calibration_min"] - ): - _new_trv_calibration = float(self.real_trvs[entity_id]["local_calibration_min"]) + # limit new setpoint within min/max of the TRV's range + t_min = float(self.real_trvs[entity_id]["local_calibration_min"]) + t_max = float(self.real_trvs[entity_id]["local_calibration_max"]) + _new_trv_calibration = max(t_min, min(_new_trv_calibration, t_max)) - _new_trv_calibration = convert_to_float( - str(_new_trv_calibration), self.name, _context - ) + + _new_trv_calibration = _convert_to_float(_new_trv_calibration) _logmsg = ( "better_thermostat %s: %s - new local calibration: %s | external_temp: %s, " @@ -128,7 +141,7 @@ def calculate_calibration_local(self, entity_id) -> Union[float, None]: _LOGGER.debug( _logmsg, - self.name, + self.device_name, entity_id, _new_trv_calibration, _cur_external_temp, @@ -139,7 +152,7 @@ def calculate_calibration_local(self, entity_id) -> Union[float, None]: return _new_trv_calibration -def calculate_calibration_setpoint(self, entity_id) -> Union[float, None]: +def calculate_calibration_setpoint(self, entity_id) -> float | None: """Calculate new setpoint for the TRV based on its own temperature measurement and the air temperature of the external sensor. This calibration is for devices with no local calibration option, it syncs the target temperature of the TRV to a new target @@ -158,10 +171,20 @@ def calculate_calibration_setpoint(self, entity_id) -> Union[float, None]: if None in (self.cur_temp, self.bt_target_temp): return None + # Add tolerance check + _within_tolerance = self.cur_temp >= ( + self.bt_target_temp - self.tolerance + ) and self.cur_temp <= (self.bt_target_temp + self.tolerance) + + if _within_tolerance: + # When within tolerance, don't adjust calibration + return self.real_trvs[entity_id]["last_temperature"] + _cur_trv_temp_s = self.real_trvs[entity_id]["current_temperature"] _cur_external_temp = self.cur_temp _cur_target_temp = self.bt_target_temp + _trv_temp_steps = 1 / ( self.real_trvs[entity_id]["target_temp_step"] or 0.5 ) if None in (_cur_target_temp, _cur_external_temp, _cur_trv_temp_s): return None @@ -197,17 +220,25 @@ def calculate_calibration_setpoint(self, entity_id) -> Union[float, None]: CONF_PROTECT_OVERHEATING, False ) + # Base calibration adjustment considering tolerance + if _cur_external_temp >= _cur_target_temp + self.tolerance: + _calibrated_setpoint -= ( + _cur_external_temp - (_cur_target_temp + self.tolerance) + ) * 2.0 + + # Additional adjustment if overheating protection is enabled if _overheating_protection is True: - if _cur_external_temp >= _cur_target_temp: - _calibrated_setpoint -= (_cur_external_temp - _cur_target_temp) * 10.0 + if _cur_external_temp >= _cur_target_temp + self.tolerance: + _calibrated_setpoint -= ( + _cur_external_temp - (_cur_target_temp + self.tolerance) + ) * 8.0 # Reduced from 10.0 since we already subtract 2.0 - _calibrated_setpoint = round_down_to_half_degree(_calibrated_setpoint) + _calibrated_setpoint = round_by_steps(_calibrated_setpoint, _trv_temp_steps) - # check if new setpoint is inside the TRV's range, else set to min or max - if _calibrated_setpoint < self.real_trvs[entity_id]["min_temp"]: - _calibrated_setpoint = self.real_trvs[entity_id]["min_temp"] - if _calibrated_setpoint > self.real_trvs[entity_id]["max_temp"]: - _calibrated_setpoint = self.real_trvs[entity_id]["max_temp"] + # limit new setpoint within min/max of the TRV's range + t_min = self.real_trvs[entity_id]["min_temp"] + t_max = self.real_trvs[entity_id]["max_temp"] + _calibrated_setpoint = max(t_min, min(_calibrated_setpoint, t_max)) _logmsg = ( "better_thermostat %s: %s - new setpoint calibration: %s | external_temp: %s, " @@ -216,7 +247,7 @@ def calculate_calibration_setpoint(self, entity_id) -> Union[float, None]: _LOGGER.debug( _logmsg, - self.name, + self.device_name, entity_id, _calibrated_setpoint, _cur_external_temp, diff --git a/custom_components/better_thermostat/climate.py b/custom_components/better_thermostat/climate.py index 591fe9cb..0672b7d5 100644 --- a/custom_components/better_thermostat/climate.py +++ b/custom_components/better_thermostat/climate.py @@ -1,54 +1,39 @@ """Better Thermostat""" import asyncio +import json import logging from abc import ABC from datetime import datetime, timedelta from random import randint from statistics import mean -from custom_components.better_thermostat.events.cooler import trigger_cooler_change - -from .utils.watcher import check_all_entities - -from .utils.weather import check_ambient_air_temperature, check_weather -from .adapters.delegate import ( - get_current_offset, - get_offset_steps, - get_min_offset, - get_max_offset, - init, - load_adapter, -) - -from .model_fixes.model_quirks import load_model_quirks - -from .utils.helpers import convert_to_float, find_battery_entity, get_hvac_bt_mode -from homeassistant.helpers import entity_platform -from homeassistant.core import callback, CoreState, Context, ServiceCall -import json +# Home Assistant imports from homeassistant.components.climate import ( - ClimateEntity, ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + ClimateEntity, PRESET_NONE, ) from homeassistant.components.climate.const import ( ATTR_MAX_TEMP, ATTR_MIN_TEMP, ATTR_TARGET_TEMP_STEP, - HVACMode, - HVACAction, ClimateEntityFeature, + HVACAction, + HVACMode, ) +from homeassistant.components.group.util import reduce_attribute from homeassistant.const import ( + ATTR_TEMPERATURE, CONF_NAME, EVENT_HOMEASSISTANT_START, - ATTR_TEMPERATURE, STATE_UNAVAILABLE, STATE_UNKNOWN, ) +from homeassistant.core import Context, CoreState, ServiceCall, callback +from homeassistant.helpers import entity_platform from homeassistant.helpers.event import ( async_track_state_change_event, async_track_time_change, @@ -56,18 +41,31 @@ ) from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.components.group.util import reduce_attribute - +# Local imports +from .adapters.delegate import ( + get_current_offset, + get_max_offset, + get_min_offset, + get_offset_steps, + init, + load_adapter, +) +from .events.cooler import trigger_cooler_change +from .events.temperature import trigger_temperature_change +from .events.trv import trigger_trv_change +from .events.window import trigger_window_change, window_queue +from .model_fixes.model_quirks import load_model_quirks from .utils.const import ( ATTR_STATE_BATTERIES, ATTR_STATE_CALL_FOR_HEAT, ATTR_STATE_ERRORS, + ATTR_STATE_HEATING_POWER, ATTR_STATE_HUMIDIY, ATTR_STATE_LAST_CHANGE, ATTR_STATE_MAIN_MODE, - ATTR_STATE_WINDOW_OPEN, ATTR_STATE_SAVED_TEMPERATURE, - ATTR_STATE_HEATING_POWER, + ATTR_STATE_WINDOW_OPEN, + BETTERTHERMOSTAT_SET_TEMPERATURE_SCHEMA, CONF_COOLER, CONF_HEATER, CONF_HUMIDITY, @@ -76,24 +74,22 @@ CONF_OUTDOOR_SENSOR, CONF_SENSOR, CONF_SENSOR_WINDOW, - CONF_TOLERANCE, CONF_TARGET_TEMP_STEP, + CONF_TOLERANCE, CONF_WEATHER, CONF_WINDOW_TIMEOUT, CONF_WINDOW_TIMEOUT_AFTER, + SERVICE_RESET_HEATING_POWER, SERVICE_RESTORE_SAVED_TARGET_TEMPERATURE, + SERVICE_SET_TEMP_TARGET_TEMPERATURE, SUPPORT_FLAGS, VERSION, - SERVICE_SET_TEMP_TARGET_TEMPERATURE, - SERVICE_RESET_HEATING_POWER, - BETTERTHERMOSTAT_SET_TEMPERATURE_SCHEMA, - BetterThermostatEntityFeature, ) - from .utils.controlling import control_queue, control_trv -from .events.temperature import trigger_temperature_change -from .events.trv import trigger_trv_change -from .events.window import trigger_window_change, window_queue +from .utils.helpers import convert_to_float, find_battery_entity, get_hvac_bt_mode +from .utils.watcher import check_all_entities +from .utils.weather import check_ambient_air_temperature, check_weather + _LOGGER = logging.getLogger(__name__) DOMAIN = "better_thermostat" @@ -103,33 +99,64 @@ class ContinueLoop(Exception): pass +@callback +def async_set_temperature_service_validate(service_call: ServiceCall) -> ServiceCall: + """Validate temperature inputs for set_temperature service.""" + if ATTR_TEMPERATURE in service_call.data: + temp = service_call.data[ATTR_TEMPERATURE] + if not isinstance(temp, (int, float)): + raise ValueError(f"Invalid temperature value {temp}, must be numeric") + + if ATTR_TARGET_TEMP_HIGH in service_call.data: + temp_high = service_call.data[ATTR_TARGET_TEMP_HIGH] + if not isinstance(temp_high, (int, float)): + raise ValueError( + f"Invalid target high temperature value {temp_high}, must be numeric" + ) + + if ATTR_TARGET_TEMP_LOW in service_call.data: + temp_low = service_call.data[ATTR_TARGET_TEMP_LOW] + if not isinstance(temp_low, (int, float)): + raise ValueError( + f"Invalid target low temperature value {temp_low}, must be numeric" + ) + + return service_call + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Better Thermostat platform.""" + platform = entity_platform.async_get_current_platform() + + # Register service validators + platform.async_register_service_validator( + "set_temperature", async_set_temperature_service_validate + ) + + async def async_setup_entry(hass, entry, async_add_devices): """Setup sensor platform.""" - async def async_service_handler(self, data: ServiceCall): - _LOGGER.debug(f"Service call: {self} » {data.service}") - if data.service == SERVICE_RESTORE_SAVED_TARGET_TEMPERATURE: - await self.restore_temp_temperature() - elif data.service == SERVICE_SET_TEMP_TARGET_TEMPERATURE: - await self.set_temp_temperature(data.data[ATTR_TEMPERATURE]) - elif data.service == SERVICE_RESET_HEATING_POWER: - await self.reset_heating_power() + async def async_service_handler(entity, call): + """Handle the service calls.""" + if call.service == SERVICE_RESTORE_SAVED_TARGET_TEMPERATURE: + await entity.restore_temp_temperature() + elif call.service == SERVICE_SET_TEMP_TARGET_TEMPERATURE: + await entity.set_temp_temperature(call.data[ATTR_TEMPERATURE]) + elif call.service == SERVICE_RESET_HEATING_POWER: + await entity.reset_heating_power() platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_SET_TEMP_TARGET_TEMPERATURE, - BETTERTHERMOSTAT_SET_TEMPERATURE_SCHEMA, # type: ignore - async_service_handler, - [ - BetterThermostatEntityFeature.TARGET_TEMPERATURE, - BetterThermostatEntityFeature.TARGET_TEMPERATURE_RANGE, - ], + BETTERTHERMOSTAT_SET_TEMPERATURE_SCHEMA, + "set_temp_temperature", ) platform.async_register_entity_service( - SERVICE_RESTORE_SAVED_TARGET_TEMPERATURE, {}, async_service_handler + SERVICE_RESTORE_SAVED_TARGET_TEMPERATURE, {}, "restore_temp_temperature" ) platform.async_register_entity_service( - SERVICE_RESET_HEATING_POWER, {}, async_service_handler + SERVICE_RESET_HEATING_POWER, {}, "reset_heating_power" ) async_add_devices( @@ -166,25 +193,23 @@ class BetterThermostat(ClimateEntity, RestoreEntity, ABC): _enable_turn_on_off_backwards_compatibility = False async def set_temp_temperature(self, temperature): + """Set temporary target temperature.""" if self._saved_temperature is None: self._saved_temperature = self.bt_target_temp self.bt_target_temp = convert_to_float( - temperature, self.device_name, "service.settarget_temperature()" + temperature, self.device_name, "service.set_temp_temperature()" ) self.async_write_ha_state() await self.control_queue_task.put(self) else: self.bt_target_temp = convert_to_float( - temperature, self.device_name, "service.settarget_temperature()" + temperature, self.device_name, "service.set_temp_temperature()" ) self.async_write_ha_state() await self.control_queue_task.put(self) - async def savetarget_temperature(self): - self._saved_temperature = self.bt_target_temp - self.async_write_ha_state() - async def restore_temp_temperature(self): + """Restore the previously saved target temperature.""" if self._saved_temperature is not None: self.bt_target_temp = convert_to_float( self._saved_temperature, @@ -196,6 +221,7 @@ async def restore_temp_temperature(self): await self.control_queue_task.put(self) async def reset_heating_power(self): + """Reset heating power to default value.""" self.heating_power = 0.01 self.async_write_ha_state() @@ -262,7 +288,7 @@ def __init__( hours=randint(1, 24 * 5) ) self.cur_temp = None - self.cur_humidity = 0 + self._current_humidity = 0 self.window_open = None self.bt_target_temp_step = float(target_temp_step) or 0.0 self.bt_min_temp = 0 @@ -346,6 +372,7 @@ async def async_added_to_hass(self): "advanced": trv["advanced"], "ignore_trv_states": False, "valve_position": None, + "valve_position_entity": None, "max_temp": None, "min_temp": None, "target_temp_step": None, @@ -435,7 +462,7 @@ async def _trigger_humidity_change(self, event): self.async_set_context(event.context) if (event.data.get("new_state")) is None: return - self.cur_humidity = convert_to_float( + self._current_humidity = convert_to_float( str(self.hass.states.get(self.humidity_entity_id).state), self.device_name, "humidity_update", @@ -552,7 +579,8 @@ async def startup(self): continue if self.humidity_entity_id is not None: - if self.hass.states.get(self.humidity_entity_id).state in ( + humidity_state = self.hass.states.get(self.humidity_entity_id) + if humidity_state is None or humidity_state.state in ( STATE_UNAVAILABLE, STATE_UNKNOWN, None, @@ -614,7 +642,7 @@ async def startup(self): ) if self.humidity_entity_id is not None: self.all_entities.append(self.humidity_entity_id) - self.cur_humidity = convert_to_float( + self._current_humidity = convert_to_float( str(self.hass.states.get(self.humidity_entity_id).state), self.device_name, "startup()", @@ -705,7 +733,9 @@ async def startup(self): "startup()", ) if old_state.attributes.get(ATTR_STATE_HUMIDIY, None) is not None: - self.cur_humidity = old_state.attributes.get(ATTR_STATE_HUMIDIY) + self._current_humidity = old_state.attributes.get( + ATTR_STATE_HUMIDIY + ) if old_state.attributes.get(ATTR_STATE_MAIN_MODE, None) is not None: self.last_main_hvac_mode = old_state.attributes.get( ATTR_STATE_MAIN_MODE @@ -717,7 +747,9 @@ async def startup(self): else: # No previous state, try and restore defaults - if self.bt_target_temp is None or type(self.bt_target_temp) != float: + if self.bt_target_temp is None or not isinstance( + self.bt_target_temp, float + ): _LOGGER.info( "better_thermostat %s: No previously saved temperature found on startup, get it from the TRV", self.device_name, @@ -771,13 +803,13 @@ async def startup(self): self.last_main_hvac_mode = self.bt_hvac_mode if self.humidity_entity_id is not None: - self.cur_humidity = convert_to_float( + self._current_humidity = convert_to_float( str(self.hass.states.get(self.humidity_entity_id).state), self.device_name, "startup()", ) else: - self.cur_humidity = 0 + self._current_humidity = 0 self.last_window_state = self.window_open if self.bt_hvac_mode not in ( @@ -827,7 +859,7 @@ async def startup(self): ) self.real_trvs[trv]["target_temp_step"] = convert_to_float( str( - self.hass.states.get(trv).attributes.get("target_temp_step", 1) + self.hass.states.get(trv).attributes.get("target_temp_step", 0.5) ), self.device_name, "startup", @@ -998,7 +1030,7 @@ async def calculate_heating_power(self): self.async_write_ha_state() @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, any]: """Return the device specific state attributes. Returns @@ -1009,9 +1041,9 @@ def extra_state_attributes(self): dev_specific = { ATTR_STATE_WINDOW_OPEN: self.window_open, ATTR_STATE_CALL_FOR_HEAT: self.call_for_heat, - ATTR_STATE_LAST_CHANGE: self.last_change, + ATTR_STATE_LAST_CHANGE: self.last_change.isoformat(), ATTR_STATE_SAVED_TEMPERATURE: self._saved_temperature, - ATTR_STATE_HUMIDIY: self.cur_humidity, + ATTR_STATE_HUMIDIY: self._current_humidity, ATTR_STATE_MAIN_MODE: self.last_main_hvac_mode, CONF_TOLERANCE: self.tolerance, CONF_TARGET_TEMP_STEP: self.bt_target_temp_step, @@ -1067,13 +1099,13 @@ def precision(self): return super().precision @property - def target_temperature_step(self): + def target_temperature_step(self) -> float | None: """Return the supported step of target temperature. Returns ------- float - Steps of target temperature. + Step size of target temperature. """ if self.bt_target_temp_step is not None: return self.bt_target_temp_step @@ -1081,45 +1113,40 @@ def target_temperature_step(self): return super().precision @property - def temperature_unit(self): - """Return the unit of measurement. - - Returns - ------- - string - The unit of measurement. - """ + def temperature_unit(self) -> str: + """Return the unit of measurement.""" return self._unit @property - def current_temperature(self): - """Return the sensor temperature. - - Returns - ------- - float - The measured temperature. - """ + def current_temperature(self) -> float | None: + """Return the current temperature.""" return self.cur_temp @property - def hvac_mode(self): - """Return current operation. + def current_humidity(self) -> float | None: + """Return the current humidity if supported.""" + return self._current_humidity if hasattr(self, "_current_humidity") else None - Returns - ------- - string - HVAC mode only from homeassistant.components.climate.const is valid - """ + @property + def hvac_mode(self) -> HVACMode | None: + """Return current operation.""" return get_hvac_bt_mode(self, self.bt_hvac_mode) + @property + def hvac_modes(self) -> list[HVACMode]: + """Return the list of available operation modes.""" + return self._hvac_list + @property def hvac_action(self): """Return the current HVAC action""" if self.bt_target_temp is not None and self.cur_temp is not None: if self.hvac_mode == HVACMode.OFF: self.attr_hvac_action = HVACAction.OFF - elif self.bt_target_temp > self.cur_temp and self.window_open is False: + elif ( + self.bt_target_temp > self.cur_temp + self.tolerance + and self.window_open is False + ): self.attr_hvac_action = HVACAction.HEATING elif ( self.bt_target_temp > self.cur_temp @@ -1162,17 +1189,6 @@ def target_temperature_high(self) -> float | None: return None return self.bt_target_cooltemp - @property - def hvac_modes(self): - """List of available operation modes. - - Returns - ------- - array - A list of HVAC modes only from homeassistant.components.climate.const is valid - """ - return self._hvac_list - async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set hvac mode. @@ -1192,17 +1208,10 @@ async def async_set_hvac_mode(self, hvac_mode: str) -> None: await self.control_queue_task.put(self) async def async_set_temperature(self, **kwargs) -> None: - """Set new target temperature. - - Parameters - ---------- - kwargs : - Arguments piped from HA. + """Set new target temperature.""" + if self.bt_hvac_mode == HVACMode.OFF: + return - Returns - ------- - None - """ _new_setpoint = None _new_setpointlow = None _new_setpointhigh = None @@ -1223,12 +1232,14 @@ async def async_set_temperature(self, **kwargs) -> None: self.device_name, "controlling.settarget_temperature()", ) + if ATTR_TARGET_TEMP_LOW in kwargs: _new_setpointlow = convert_to_float( str(kwargs.get(ATTR_TARGET_TEMP_LOW, None)), self.device_name, "controlling.settarget_temperature_low()", ) + if ATTR_TARGET_TEMP_HIGH in kwargs: _new_setpointhigh = convert_to_float( str(kwargs.get(ATTR_TARGET_TEMP_HIGH, None)), @@ -1241,6 +1252,17 @@ async def async_set_temperature(self, **kwargs) -> None: f"better_thermostat {self.device_name}: received a new setpoint from HA, but temperature attribute was not set, ignoring" ) return + + # Validate against min/max temps + if _new_setpoint is not None: + _new_setpoint = min(self.max_temp, max(self.min_temp, _new_setpoint)) + if _new_setpointlow is not None: + _new_setpointlow = min(self.max_temp, max(self.min_temp, _new_setpointlow)) + if _new_setpointhigh is not None: + _new_setpointhigh = min( + self.max_temp, max(self.min_temp, _new_setpointhigh) + ) + self.bt_target_temp = _new_setpoint or _new_setpointlow if _new_setpointhigh is not None: self.bt_target_cooltemp = _new_setpointhigh diff --git a/custom_components/better_thermostat/config_flow.py b/custom_components/better_thermostat/config_flow.py index 93709417..2c924ee5 100644 --- a/custom_components/better_thermostat/config_flow.py +++ b/custom_components/better_thermostat/config_flow.py @@ -21,7 +21,7 @@ CONF_CHILD_LOCK, CONF_HEAT_AUTO_SWAPPED, CONF_HEATER, - CONF_HOMATICIP, + CONF_HOMEMATICIP, CONF_HUMIDITY, CONF_MODEL, CONF_NO_SYSTEM_MODE_OFF, @@ -111,7 +111,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize the config flow.""" - self.name = "" + self.device_name = "" self.data = None self.model = None self.heater_entity_id = None @@ -246,7 +246,7 @@ async def async_step_advanced(self, user_input=None, _trv_config=None): ] = bool fields[ vol.Optional( - CONF_HOMATICIP, default=user_input.get(CONF_HOMATICIP, homematic) + CONF_HOMEMATICIP, default=user_input.get(CONF_HOMEMATICIP, homematic) ) ] = bool @@ -394,7 +394,7 @@ def __init__(self, config_entry: config_entries.ConfigEntry) -> None: self.options = dict(config_entry.options) self.i = 0 self.trv_bundle = [] - self.name = "" + self.device_name = "" self._last_step = False self.updated_config = {} @@ -436,7 +436,7 @@ async def async_step_advanced( fields = OrderedDict() _default_calibration = "target_temp_based" - self.name = user_input.get(CONF_NAME, "-") + self.device_name = user_input.get(CONF_NAME, "-") _adapter = await load_adapter( self, _trv_config.get("integration"), _trv_config.get("trv") @@ -523,8 +523,8 @@ async def async_step_advanced( ] = bool fields[ vol.Optional( - CONF_HOMATICIP, - default=_trv_config["advanced"].get(CONF_HOMATICIP, homematic), + CONF_HOMEMATICIP, + default=_trv_config["advanced"].get(CONF_HOMEMATICIP, homematic), ) ] = bool diff --git a/custom_components/better_thermostat/device_action.py b/custom_components/better_thermostat/device_action.py new file mode 100644 index 00000000..602a3d9f --- /dev/null +++ b/custom_components/better_thermostat/device_action.py @@ -0,0 +1,123 @@ +"""Provides device actions for Better Thermostat.""" + +from __future__ import annotations + +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_ENTITY_ID, + CONF_TYPE, +) +from homeassistant.core import Context, HomeAssistant +from homeassistant.helpers import entity_registry +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType, TemplateVarsType + +from . import DOMAIN +from homeassistant.components.climate import ( + ATTR_HVAC_MODE, + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + ATTR_TEMPERATURE, + DOMAIN as CLIMATE_DOMAIN, + HVACMode, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_TEMPERATURE, +) + +ACTION_TYPES = {"set_hvac_mode", "set_temperature"} + +_ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): vol.In(ACTION_TYPES), + vol.Required(CONF_ENTITY_ID): cv.entity_domain(CLIMATE_DOMAIN), + } +) + + +async def async_get_actions( + hass: HomeAssistant, device_id: str +) -> list[dict[str, str]]: + """List device actions for Better Thermostat devices.""" + registry = entity_registry.async_get(hass) + actions = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + base_action = { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + } + + actions.extend( + [ + {**base_action, CONF_TYPE: "set_hvac_mode"}, + {**base_action, CONF_TYPE: "set_temperature"}, + ] + ) + + return actions + + +async def async_call_action_from_config( + hass: HomeAssistant, + config: ConfigType, + variables: TemplateVarsType, + context: Context | None, +) -> None: + """Execute a device action.""" + service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]} + + if config[CONF_TYPE] == "set_hvac_mode": + service = SERVICE_SET_HVAC_MODE + service_data[ATTR_HVAC_MODE] = config[ATTR_HVAC_MODE] + else: # config[CONF_TYPE] == "set_temperature" + service = SERVICE_SET_TEMPERATURE + if ATTR_TARGET_TEMP_HIGH in config: + service_data[ATTR_TARGET_TEMP_HIGH] = config[ATTR_TARGET_TEMP_HIGH] + if ATTR_TARGET_TEMP_LOW in config: + service_data[ATTR_TARGET_TEMP_LOW] = config[ATTR_TARGET_TEMP_LOW] + if ATTR_TEMPERATURE in config: + service_data[ATTR_TEMPERATURE] = config[ATTR_TEMPERATURE] + + await hass.services.async_call( + CLIMATE_DOMAIN, service, service_data, blocking=True, context=context + ) + + +async def async_get_action_capabilities( + hass: HomeAssistant, config: ConfigType +) -> dict[str, vol.Schema]: + """List action capabilities.""" + action_type = config[CONF_TYPE] + + if action_type == "set_hvac_mode": + return { + "extra_fields": vol.Schema( + { + vol.Required(ATTR_HVAC_MODE): vol.In( + [HVACMode.HEAT, HVACMode.OFF, HVACMode.HEAT_COOL] + ) + } + ) + } + + if action_type == "set_temperature": + return { + "extra_fields": vol.Schema( + { + vol.Optional(ATTR_TEMPERATURE): vol.Coerce(float), + vol.Optional(ATTR_TARGET_TEMP_HIGH): vol.Coerce(float), + vol.Optional(ATTR_TARGET_TEMP_LOW): vol.Coerce(float), + } + ) + } + + return {} diff --git a/custom_components/better_thermostat/device_condition.py b/custom_components/better_thermostat/device_condition.py new file mode 100644 index 00000000..29195dde --- /dev/null +++ b/custom_components/better_thermostat/device_condition.py @@ -0,0 +1,138 @@ +"""Provide device conditions for Better Thermostat.""" + +from __future__ import annotations + +import voluptuous as vol + +from homeassistant.const import ( + CONF_CONDITION, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_ENTITY_ID, + CONF_TYPE, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import condition, config_validation as cv, entity_registry +from homeassistant.helpers.typing import ConfigType + +from . import DOMAIN +from homeassistant.components.climate.const import ( + ATTR_HVAC_ACTION, + ATTR_HVAC_MODE, + HVACAction, + HVACMode, +) + +CONDITION_TYPES = {"is_hvac_mode", "is_hvac_action"} + +HVAC_MODE_CONDITION = vol.Schema( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): "is_hvac_mode", + vol.Required(ATTR_HVAC_MODE): vol.In( + [HVACMode.OFF, HVACMode.HEAT, HVACMode.HEAT_COOL] + ), + } +) + +HVAC_ACTION_CONDITION = vol.Schema( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): "is_hvac_action", + vol.Required(ATTR_HVAC_ACTION): vol.In( + [HVACAction.OFF, HVACAction.HEATING, HVACAction.IDLE] + ), + } +) + +CONDITION_SCHEMA = vol.Any(HVAC_MODE_CONDITION, HVAC_ACTION_CONDITION) + + +async def async_get_conditions( + hass: HomeAssistant, device_id: str +) -> list[dict[str, str]]: + """List device conditions for Better Thermostat devices.""" + registry = entity_registry.async_get(hass) + conditions = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + base_condition = { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + } + + conditions.extend( + [{**base_condition, CONF_TYPE: cond} for cond in CONDITION_TYPES] + ) + + return conditions + + +@callback +def async_condition_from_config( + hass: HomeAssistant, config: ConfigType +) -> condition.ConditionCheckerType: + """Create a function to test a device condition.""" + if config[CONF_TYPE] == "is_hvac_mode": + hvac_mode = config[ATTR_HVAC_MODE] + + def test_is_hvac_mode(hass: HomeAssistant, variables: dict) -> bool: + """Test if an HVAC mode condition is met.""" + state = hass.states.get(config[CONF_ENTITY_ID]) + return ( + state is not None and state.attributes.get(ATTR_HVAC_MODE) == hvac_mode + ) + + return test_is_hvac_mode + + if config[CONF_TYPE] == "is_hvac_action": + hvac_action = config[ATTR_HVAC_ACTION] + + def test_is_hvac_action(hass: HomeAssistant, variables: dict) -> bool: + """Test if an HVAC action condition is met.""" + state = hass.states.get(config[CONF_ENTITY_ID]) + return ( + state is not None + and state.attributes.get(ATTR_HVAC_ACTION) == hvac_action + ) + + return test_is_hvac_action + + return lambda *_: False + + +async def async_get_condition_capabilities( + hass: HomeAssistant, config: ConfigType +) -> dict[str, vol.Schema]: + """List condition capabilities.""" + condition_type = config[CONF_TYPE] + + if condition_type == "is_hvac_mode": + return { + "extra_fields": vol.Schema( + { + vol.Required(ATTR_HVAC_MODE): vol.In( + [HVACMode.OFF, HVACMode.HEAT, HVACMode.HEAT_COOL] + ) + } + ) + } + + if condition_type == "is_hvac_action": + return { + "extra_fields": vol.Schema( + { + vol.Required(ATTR_HVAC_ACTION): vol.In( + [HVACAction.OFF, HVACAction.HEATING, HVACAction.IDLE] + ) + } + ) + } + + return {} diff --git a/custom_components/better_thermostat/device_trigger.py b/custom_components/better_thermostat/device_trigger.py index ec354f50..4f9523df 100644 --- a/custom_components/better_thermostat/device_trigger.py +++ b/custom_components/better_thermostat/device_trigger.py @@ -11,11 +11,7 @@ from . import DOMAIN from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType -from homeassistant.components.climate.const import ( - ATTR_CURRENT_TEMPERATURE, - ATTR_CURRENT_HUMIDITY, - HVAC_MODES, -) +from homeassistant.components.climate.const import HVAC_MODES from homeassistant.const import ( CONF_ABOVE, CONF_BELOW, @@ -32,7 +28,7 @@ async def async_get_triggers( hass: HomeAssistant, device_id: str ) -> list[dict[str, str]]: - """List device triggers for Climate devices.""" + """List device triggers for Better Thermostat devices.""" registry = entity_registry.async_get(hass) triggers = [] @@ -42,8 +38,9 @@ async def async_get_triggers( continue state = hass.states.get(entry.entity_id) + if not state: + continue - # Add triggers for each entity that belongs to this integration base_trigger = { CONF_PLATFORM: "device", CONF_DEVICE_ID: device_id, @@ -51,13 +48,26 @@ async def async_get_triggers( CONF_ENTITY_ID: entry.entity_id, } - triggers.append({**base_trigger, CONF_TYPE: "hvac_mode_changed"}) - - if state and ATTR_CURRENT_TEMPERATURE in state.attributes: - triggers.append({**base_trigger, CONF_TYPE: "current_temperature_changed"}) - - if state and ATTR_CURRENT_HUMIDITY in state.attributes: - triggers.append({**base_trigger, CONF_TYPE: "current_humidity_changed"}) + # Add standard climate triggers + triggers.extend( + [ + { + **base_trigger, + CONF_TYPE: "hvac_mode_changed", + "metadata": {"secondary": False}, + }, + { + **base_trigger, + CONF_TYPE: "current_temperature_changed", + "metadata": {"secondary": False}, + }, + { + **base_trigger, + CONF_TYPE: "current_humidity_changed", + "metadata": {"secondary": True}, + }, + ] + ) return triggers @@ -122,31 +132,35 @@ async def async_get_trigger_capabilities( """List trigger capabilities.""" trigger_type = config[CONF_TYPE] - if trigger_type == "hvac_action_changed": - return {} - if trigger_type == "hvac_mode_changed": return { "extra_fields": vol.Schema( - {vol.Optional(CONF_FOR): cv.positive_time_period_dict} + { + vol.Required(state_trigger.CONF_TO): vol.In(HVAC_MODES), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, + } ) } - if trigger_type == "current_temperature_changed": - unit_of_measurement = hass.config.units.temperature_unit - else: - unit_of_measurement = PERCENTAGE - - return { - "extra_fields": vol.Schema( - { - vol.Optional( - CONF_ABOVE, description={"suffix": unit_of_measurement} - ): vol.Coerce(float), - vol.Optional( - CONF_BELOW, description={"suffix": unit_of_measurement} - ): vol.Coerce(float), - vol.Optional(CONF_FOR): cv.positive_time_period_dict, - } + # Temperature and humidity triggers use the same schema + if trigger_type in ["current_temperature_changed", "current_humidity_changed"]: + unit = ( + hass.config.units.temperature_unit + if trigger_type == "current_temperature_changed" + else PERCENTAGE ) - } + return { + "extra_fields": vol.Schema( + { + vol.Optional(CONF_ABOVE, description={"suffix": unit}): vol.Coerce( + float + ), + vol.Optional(CONF_BELOW, description={"suffix": unit}): vol.Coerce( + float + ), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, + } + ) + } + + return {} diff --git a/custom_components/better_thermostat/diagnostics.py b/custom_components/better_thermostat/diagnostics.py index 780aa4ca..c60c7f82 100644 --- a/custom_components/better_thermostat/diagnostics.py +++ b/custom_components/better_thermostat/diagnostics.py @@ -19,16 +19,18 @@ async def async_get_config_entry_diagnostics( trv = hass.states.get(trv_id["trv"]) if trv is None: continue - _adapter_name = await load_adapter( - hass, trv_id["integration"], trv_id["trv"], True - ) - trv_id["adapter"] = _adapter_name + # TODO: this does nothing but return trv_id["integration"] as adapter_name + # -> removing this for now, to fix diagnostic export + # _adapter_name = await load_adapter( + # hass, trv_id["integration"], trv_id["trv"], True + # ) + # trv_id["adapter"] = _adapter_name trvs[trv_id["trv"]] = { "name": trv.name, "state": trv.state, "attributes": trv.attributes, "bt_config": trv_id["advanced"], - "bt_adapter": trv_id["adapter"], + # "bt_adapter": trv_id["adapter"], "bt_integration": trv_id["integration"], "model": trv_id["model"], } diff --git a/custom_components/better_thermostat/events/cooler.py b/custom_components/better_thermostat/events/cooler.py index 9595ab81..dfb2f8bf 100644 --- a/custom_components/better_thermostat/events/cooler.py +++ b/custom_components/better_thermostat/events/cooler.py @@ -29,13 +29,13 @@ async def trigger_cooler_change(self, event): if None in (new_state, old_state, new_state.attributes): _LOGGER.debug( - f"better_thermostat {self.name}: Cooler {entity_id} update contained not all necessary data for processing, skipping" + f"better_thermostat {self.device_name}: Cooler {entity_id} update contained not all necessary data for processing, skipping" ) return if not isinstance(new_state, State) or not isinstance(old_state, State): _LOGGER.debug( - f"better_thermostat {self.name}: Cooler {entity_id} update contained not a State, skipping" + f"better_thermostat {self.device_name}: Cooler {entity_id} update contained not a State, skipping" ) return # set context HACK TO FIND OUT IF AN EVENT WAS SEND BY BT @@ -44,7 +44,9 @@ async def trigger_cooler_change(self, event): if self.context == event.context: return - _LOGGER.debug(f"better_thermostat {self.name}: Cooler {entity_id} update received") + _LOGGER.debug( + f"better_thermostat {self.device_name}: Cooler {entity_id} update received" + ) _main_key = "temperature" if "temperature" not in old_state.attributes: @@ -52,12 +54,12 @@ async def trigger_cooler_change(self, event): _old_cooling_setpoint = convert_to_float( str(old_state.attributes.get(_main_key, None)), - self.name, + self.device_name, "trigger_cooler_change()", ) _new_cooling_setpoint = convert_to_float( str(new_state.attributes.get(_main_key, None)), - self.name, + self.device_name, "trigger_cooler_change()", ) if ( @@ -66,14 +68,14 @@ async def trigger_cooler_change(self, event): and self.bt_hvac_mode is not HVACMode.OFF ): _LOGGER.debug( - f"better_thermostat {self.name}: trigger_cooler_change / _old_cooling_setpoint: {_old_cooling_setpoint} - _new_cooling_setpoint: {_new_cooling_setpoint}" + f"better_thermostat {self.device_name}: trigger_cooler_change / _old_cooling_setpoint: {_old_cooling_setpoint} - _new_cooling_setpoint: {_new_cooling_setpoint}" ) if ( _new_cooling_setpoint < self.bt_min_temp or self.bt_max_temp < _new_cooling_setpoint ): _LOGGER.warning( - f"better_thermostat {self.name}: New Cooler {entity_id} setpoint outside of range, overwriting it" + f"better_thermostat {self.device_name}: New Cooler {entity_id} setpoint outside of range, overwriting it" ) if _new_cooling_setpoint < self.bt_min_temp: diff --git a/custom_components/better_thermostat/events/temperature.py b/custom_components/better_thermostat/events/temperature.py index cd12284c..6ea067b9 100644 --- a/custom_components/better_thermostat/events/temperature.py +++ b/custom_components/better_thermostat/events/temperature.py @@ -1,8 +1,9 @@ import logging -from custom_components.better_thermostat.utils.const import CONF_HOMATICIP +from custom_components.better_thermostat.utils.const import CONF_HOMEMATICIP from ..utils.helpers import convert_to_float from datetime import datetime +from homeassistant.helpers import issue_registry as ir from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import callback @@ -33,18 +34,36 @@ async def trigger_temperature_change(self, event): return _incoming_temperature = convert_to_float( - str(new_state.state), self.name, "external_temperature" + str(new_state.state), self.device_name, "external_temperature" ) - _time_diff = 5 + _time_diff = 60 try: for trv in self.all_trvs: - if trv["advanced"][CONF_HOMATICIP]: + if trv["advanced"][CONF_HOMEMATICIP]: _time_diff = 600 except KeyError: pass + if _incoming_temperature is None or _incoming_temperature < -50: + # raise a ha repair notication + _LOGGER.error( + "better_thermostat %s: external_temperature is not a valid number: %s", + self.device_name, + new_state.state, + ) + ir.async_create_issue( + hass=self.hass, + issue_id=f"missing_entity_{self.device_name}", + issue_title=f"better_thermostat {self.device_name} has invalid external_temperature value", + issue_severity="error", + issue_description=f"better_thermostat {self.device_name} has invalid external_temperature: {new_state.state}", + issue_category="config", + issue_suggested_action="Please check the external_temperature sensor", + ) + return + if ( _incoming_temperature != self.cur_temp and (datetime.now() - self.last_external_sensor_change).total_seconds() @@ -52,7 +71,7 @@ async def trigger_temperature_change(self, event): ): _LOGGER.debug( "better_thermostat %s: external_temperature changed from %s to %s", - self.name, + self.device_name, self.cur_temp, _incoming_temperature, ) diff --git a/custom_components/better_thermostat/events/trv.py b/custom_components/better_thermostat/events/trv.py index ed86168f..e2fa834b 100644 --- a/custom_components/better_thermostat/events/trv.py +++ b/custom_components/better_thermostat/events/trv.py @@ -1,7 +1,7 @@ import asyncio from datetime import datetime import logging -from custom_components.better_thermostat.utils.const import CONF_HOMATICIP +from custom_components.better_thermostat.utils.const import CONF_HOMEMATICIP from homeassistant.components.climate.const import ( HVACMode, @@ -46,13 +46,13 @@ async def trigger_trv_change(self, event): if None in (new_state, old_state, new_state.attributes): _LOGGER.debug( - f"better_thermostat {self.name}: TRV {entity_id} update contained not all necessary data for processing, skipping" + f"better_thermostat {self.device_name}: TRV {entity_id} update contained not all necessary data for processing, skipping" ) return if not isinstance(new_state, State) or not isinstance(old_state, State): _LOGGER.debug( - f"better_thermostat {self.name}: TRV {entity_id} update contained not a State, skipping" + f"better_thermostat {self.device_name}: TRV {entity_id} update contained not a State, skipping" ) return # set context HACK TO FIND OUT IF AN EVENT WAS SEND BY BT @@ -61,21 +61,21 @@ async def trigger_trv_change(self, event): if self.context == event.context: return - # _LOGGER.debug(f"better_thermostat {self.name}: TRV {entity_id} update received") + # _LOGGER.debug(f"better_thermostat {self.device_name}: TRV {entity_id} update received") _org_trv_state = self.hass.states.get(entity_id) child_lock = self.real_trvs[entity_id]["advanced"].get("child_lock") _new_current_temp = convert_to_float( str(_org_trv_state.attributes.get("current_temperature", None)), - self.name, + self.device_name, "TRV_current_temp", ) _time_diff = 5 try: for trv in self.all_trvs: - if trv["advanced"][CONF_HOMATICIP]: + if trv["advanced"][CONF_HOMEMATICIP]: _time_diff = 600 except KeyError: pass @@ -94,7 +94,7 @@ async def trigger_trv_change(self, event): _old_temp = self.real_trvs[entity_id]["current_temperature"] self.real_trvs[entity_id]["current_temperature"] = _new_current_temp _LOGGER.debug( - f"better_thermostat {self.name}: TRV {entity_id} sends new internal temperature from {_old_temp} to {_new_current_temp}" + f"better_thermostat {self.device_name}: TRV {entity_id} sends new internal temperature from {_old_temp} to {_new_current_temp}" ) self.last_internal_sensor_change = datetime.now() _main_change = True @@ -103,7 +103,7 @@ async def trigger_trv_change(self, event): if self.real_trvs[entity_id]["calibration_received"] is False: self.real_trvs[entity_id]["calibration_received"] = True _LOGGER.debug( - f"better_thermostat {self.name}: calibration accepted by TRV {entity_id}" + f"better_thermostat {self.device_name}: calibration accepted by TRV {entity_id}" ) _main_change = False if self.real_trvs[entity_id]["calibration"] == 0: @@ -118,7 +118,7 @@ async def trigger_trv_change(self, event): mapped_state = convert_inbound_states(self, entity_id, _org_trv_state) except TypeError: _LOGGER.debug( - f"better_thermostat {self.name}: remapping TRV {entity_id} state failed, skipping" + f"better_thermostat {self.device_name}: remapping TRV {entity_id} state failed, skipping" ) return @@ -129,7 +129,7 @@ async def trigger_trv_change(self, event): ): _old = self.real_trvs[entity_id]["hvac_mode"] _LOGGER.debug( - f"better_thermostat {self.name}: TRV {entity_id} decoded TRV mode changed from {_old} to {_org_trv_state.state} - converted {new_state.state}" + f"better_thermostat {self.device_name}: TRV {entity_id} decoded TRV mode changed from {_old} to {_org_trv_state.state} - converted {new_state.state}" ) self.real_trvs[entity_id]["hvac_mode"] = _org_trv_state.state _main_change = True @@ -142,16 +142,16 @@ async def trigger_trv_change(self, event): _main_key = "temperature" if "temperature" not in old_state.attributes: - _main_key = "target_temp_high" + _main_key = "target_temp_low" _old_heating_setpoint = convert_to_float( str(old_state.attributes.get(_main_key, None)), - self.name, + self.device_name, "trigger_trv_change()", ) _new_heating_setpoint = convert_to_float( str(new_state.attributes.get(_main_key, None)), - self.name, + self.device_name, "trigger_trv_change()", ) if ( @@ -163,14 +163,14 @@ async def trigger_trv_change(self, event): ) ): _LOGGER.debug( - f"better_thermostat {self.name}: trigger_trv_change test / _old_heating_setpoint: {_old_heating_setpoint} - _new_heating_setpoint: {_new_heating_setpoint} - _last_temperature: {self.real_trvs[entity_id]['last_temperature']}" + f"better_thermostat {self.device_name}: trigger_trv_change test / _old_heating_setpoint: {_old_heating_setpoint} - _new_heating_setpoint: {_new_heating_setpoint} - _last_temperature: {self.real_trvs[entity_id]['last_temperature']}" ) if ( _new_heating_setpoint < self.bt_min_temp or self.bt_max_temp < _new_heating_setpoint ): _LOGGER.warning( - f"better_thermostat {self.name}: New TRV {entity_id} setpoint outside of range, overwriting it" + f"better_thermostat {self.device_name}: New TRV {entity_id} setpoint outside of range, overwriting it" ) if _new_heating_setpoint < self.bt_min_temp: @@ -189,7 +189,7 @@ async def trigger_trv_change(self, event): and self.window_open is False ): _LOGGER.debug( - f"better_thermostat {self.name}: TRV {entity_id} decoded TRV target temp changed from {self.bt_target_temp} to {_new_heating_setpoint}" + f"better_thermostat {self.device_name}: TRV {entity_id} decoded TRV target temp changed from {self.bt_target_temp} to {_new_heating_setpoint}" ) self.bt_target_temp = _new_heating_setpoint if self.cooler_entity_id is not None: @@ -230,7 +230,7 @@ async def update_hvac_action(self): # i don't know why this is here just for hometicip / wtom - 2023-08-23 # for trv in self.all_trvs: - # if trv["advanced"][CONF_HOMATICIP]: + # if trv["advanced"][CONF_HOMEMATICIP]: # entity_id = trv["trv"] # state = self.hass.states.get(entity_id) # if state is None: @@ -334,7 +334,7 @@ def convert_outbound_states(self, entity_id, hvac_mode) -> dict | None: if _calibration_type is None: _LOGGER.warning( "better_thermostat %s: no calibration type found in device config, talking to the TRV using fallback mode", - self.name, + self.device_name, ) _new_heating_setpoint = self.bt_target_temp _new_local_calibration = calculate_calibration_local(self, entity_id) @@ -357,34 +357,28 @@ def convert_outbound_states(self, entity_id, hvac_mode) -> dict | None: ) _system_modes = self.real_trvs[entity_id]["hvac_modes"] - _has_system_mode = False - if _system_modes is not None: - _has_system_mode = True + _has_system_mode = _system_modes is not None # Handling different devices with or without system mode reported or contained in the device config hvac_mode = mode_remap(self, entity_id, str(hvac_mode), False) - if _has_system_mode is False: + if not _has_system_mode: _LOGGER.debug( - f"better_thermostat {self.name}: device config expects no system mode, while the device has one. Device system mode will be ignored" + f"better_thermostat {self.device_name}: device config expects no system mode, while the device has one. Device system mode will be ignored" ) if hvac_mode == HVACMode.OFF: _new_heating_setpoint = self.real_trvs[entity_id]["min_temp"] hvac_mode = None - if ( - HVACMode.OFF not in _system_modes - or self.real_trvs[entity_id]["advanced"].get( - "no_off_system_mode", False - ) - is True + if hvac_mode == HVACMode.OFF and ( + HVACMode.OFF not in _system_modes or + self.real_trvs[entity_id]["advanced"].get("no_off_system_mode") ): - if hvac_mode == HVACMode.OFF: - _LOGGER.debug( - f"better_thermostat {self.name}: sending 5°C to the TRV because this device has no system mode off and heater should be off" - ) - _new_heating_setpoint = self.real_trvs[entity_id]["min_temp"] - hvac_mode = None + _LOGGER.debug( + f"better_thermostat {self.device_name}: sending 5°C to the TRV because this device has no system mode off and heater should be off" + ) + _new_heating_setpoint = self.real_trvs[entity_id]["min_temp"] + hvac_mode = None return { "temperature": _new_heating_setpoint, diff --git a/custom_components/better_thermostat/events/window.py b/custom_components/better_thermostat/events/window.py index 96e0fdd8..7c0ab203 100644 --- a/custom_components/better_thermostat/events/window.py +++ b/custom_components/better_thermostat/events/window.py @@ -1,8 +1,10 @@ import asyncio import logging +from custom_components.better_thermostat import DOMAIN from homeassistant.core import callback from homeassistant.const import STATE_OFF +from homeassistant.helpers import issue_registry as ir _LOGGER = logging.getLogger(__name__) @@ -37,7 +39,7 @@ async def trigger_window_change(self, event) -> None: if new_state == "unknown": _LOGGER.warning( "better_thermostat %s: Window sensor state is unknown, assuming window is open", - self.name, + self.device_name, ) # window was opened, disable heating power calculation for this period @@ -47,45 +49,65 @@ async def trigger_window_change(self, event) -> None: new_window_open = False else: _LOGGER.error( - f"better_thermostat {self.name}: New window sensor state '{new_state}' not recognized" + f"better_thermostat {self.device_name}: New window sensor state '{new_state}' not recognized" + ) + ir.async_create_issue( + hass=self.hass, + domain=DOMAIN, + issue_id=f"missing_entity_{self.device_name}", + issue_title=f"better_thermostat {self.device_name} has invalid window sensor state", + issue_severity="error", + issue_description=f"better_thermostat {self.device_name} has invalid window sensor state: {new_state}", + issue_category="config", + issue_suggested_action="Please check the window sensor", ) return # make sure to skip events which do not change the saved window state: if new_window_open == old_window_open: _LOGGER.debug( - f"better_thermostat {self.name}: Window state did not change, skipping event" + f"better_thermostat {self.device_name}: Window state did not change, skipping event" ) return await self.window_queue_task.put(new_window_open) async def window_queue(self): - while True: - window_event_to_process = await self.window_queue_task.get() - if window_event_to_process is not None: - if window_event_to_process: - _LOGGER.debug( - f"better_thermostat {self.name}: Window opened, waiting {self.window_delay} seconds before continuing" - ) - await asyncio.sleep(self.window_delay) - else: - _LOGGER.debug( - f"better_thermostat {self.name}: Window closed, waiting {self.window_delay_after} seconds before continuing" - ) - await asyncio.sleep(self.window_delay_after) - # remap off on to true false - current_window_state = True - if self.hass.states.get(self.window_id).state == STATE_OFF: - current_window_state = False - # make sure the current state is the suggested change state to prevent a false positive: - if current_window_state == window_event_to_process: - self.window_open = window_event_to_process - self.async_write_ha_state() - if not self.control_queue_task.empty(): - empty_queue(self.control_queue_task) - await self.control_queue_task.put(self) - self.window_queue_task.task_done() + try: + while True: + window_event_to_process = await self.window_queue_task.get() + try: + if window_event_to_process is not None: + if window_event_to_process: + _LOGGER.debug( + f"better_thermostat {self.device_name}: Window opened, waiting {self.window_delay} seconds before continuing" + ) + await asyncio.sleep(self.window_delay) + else: + _LOGGER.debug( + f"better_thermostat {self.device_name}: Window closed, waiting {self.window_delay_after} seconds before continuing" + ) + await asyncio.sleep(self.window_delay_after) + # remap off on to true false + current_window_state = True + if self.hass.states.get(self.window_id).state == STATE_OFF: + current_window_state = False + # make sure the current state is the suggested change state to prevent a false positive: + if current_window_state == window_event_to_process: + self.window_open = window_event_to_process + self.async_write_ha_state() + if not self.control_queue_task.empty(): + empty_queue(self.control_queue_task) + await self.control_queue_task.put(self) + except asyncio.CancelledError: + raise + finally: + self.window_queue_task.task_done() + except asyncio.CancelledError: + _LOGGER.debug( + f"better_thermostat {self.device_name}: Window queue task cancelled" + ) + raise def empty_queue(q: asyncio.Queue): diff --git a/custom_components/better_thermostat/manifest.json b/custom_components/better_thermostat/manifest.json index e42b4fbe..c1de64af 100644 --- a/custom_components/better_thermostat/manifest.json +++ b/custom_components/better_thermostat/manifest.json @@ -2,7 +2,8 @@ "domain": "better_thermostat", "name": "Better Thermostat", "after_dependencies": [ - "climate" + "climate", + "zwave_js" ], "codeowners": [ "@kartoffeltoby" @@ -16,5 +17,5 @@ "iot_class": "local_push", "issue_tracker": "https://github.com/KartoffelToby/better_thermostat/issues", "requirements": [], - "version": "1.6.2-dev" + "version": "1.7.0-beta1" } diff --git a/custom_components/better_thermostat/model_fixes/BTH-RM.py b/custom_components/better_thermostat/model_fixes/BTH-RM.py new file mode 100644 index 00000000..9af5f244 --- /dev/null +++ b/custom_components/better_thermostat/model_fixes/BTH-RM.py @@ -0,0 +1,58 @@ +# Quirks for BTH-RM +import logging +from homeassistant.helpers import device_registry as dr, entity_registry as er + +_LOGGER = logging.getLogger(__name__) + + +def fix_local_calibration(self, entity_id, offset): + return offset + + +def fix_target_temperature_calibration(self, entity_id, temperature): + return temperature + + +async def override_set_hvac_mode(self, entity_id, hvac_mode): + return False + + +async def override_set_temperature(self, entity_id, temperature): + """Bosch room thermostat BTH-RM has a quirk where it needs to set both high + and low temperature, if heat and cool modes are available in newer Z2M versions. + """ + model = self.real_trvs[entity_id]["model"] + if model == "BTH-RM": + _LOGGER.debug( + f"better_thermostat {self.name}: TRV {entity_id} device quirk bth-rm for set_temperature active" + ) + entity_reg = er.async_get(self.hass) + entry = entity_reg.async_get(entity_id) + + hvac_modes = entry.capabilities.get("hvac_modes", []) + + _LOGGER.debug( + f"better_thermostat {self.name}: TRV {entity_id} device quirk bth-rm found hvac_modes {hvac_modes}" + ) + + if entry.platform == "mqtt" and "cool" in hvac_modes and "heat" in hvac_modes: + await self.hass.services.async_call( + "climate", + "set_temperature", + { + "entity_id": entity_id, + "target_temp_high": temperature, + "target_temp_low": temperature, + }, + blocking=True, + context=self.context, + ) + else: + await self.hass.services.async_call( + "climate", + "set_temperature", + {"entity_id": entity_id, "temperature": temperature}, + blocking=True, + context=self.context, + ) + return True diff --git a/custom_components/better_thermostat/model_fixes/BTH-RM230Z.py b/custom_components/better_thermostat/model_fixes/BTH-RM230Z.py index 43f21eb2..0c7a1621 100644 --- a/custom_components/better_thermostat/model_fixes/BTH-RM230Z.py +++ b/custom_components/better_thermostat/model_fixes/BTH-RM230Z.py @@ -1,6 +1,6 @@ # Quirks for BTH-RM230Z import logging -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import entity_registry as er _LOGGER = logging.getLogger(__name__) @@ -24,7 +24,7 @@ async def override_set_temperature(self, entity_id, temperature): model = self.real_trvs[entity_id]["model"] if model == "BTH-RM230Z": _LOGGER.debug( - f"better_thermostat {self.name}: TRV {entity_id} device quirk bth-rm230z for set_temperature active" + f"better_thermostat {self.device_name}: TRV {entity_id} device quirk bth-rm230z for set_temperature active" ) entity_reg = er.async_get(self.hass) entry = entity_reg.async_get(entity_id) @@ -32,7 +32,7 @@ async def override_set_temperature(self, entity_id, temperature): hvac_modes = entry.capabilities.get("hvac_modes", []) _LOGGER.debug( - f"better_thermostat {self.name}: TRV {entity_id} device quirk bth-rm230z found hvac_modes {hvac_modes}" + f"better_thermostat {self.device_name}: TRV {entity_id} device quirk bth-rm230z found hvac_modes {hvac_modes}" ) if entry.platform == "mqtt" and "cool" in hvac_modes and "heat" in hvac_modes: diff --git a/custom_components/better_thermostat/model_fixes/TV02-Zigbee.py b/custom_components/better_thermostat/model_fixes/TV02-Zigbee.py index 6eba1577..46e8240b 100644 --- a/custom_components/better_thermostat/model_fixes/TV02-Zigbee.py +++ b/custom_components/better_thermostat/model_fixes/TV02-Zigbee.py @@ -37,7 +37,7 @@ async def override_set_hvac_mode(self, entity_id, hvac_mode): model = self.real_trvs[entity_id]["model"] if model == "TV02-Zigbee" and hvac_mode != HVACMode.OFF: _LOGGER.debug( - f"better_thermostat {self.name}: TRV {entity_id} device quirk hvac trv02-zigbee active" + f"better_thermostat {self.device_name}: TRV {entity_id} device quirk hvac trv02-zigbee active" ) await self.hass.services.async_call( "climate", @@ -66,7 +66,7 @@ async def override_set_temperature(self, entity_id, temperature): model = self.real_trvs[entity_id]["model"] if model == "TV02-Zigbee": _LOGGER.debug( - f"better_thermostat {self.name}: TRV {entity_id} device quirk trv02-zigbee active" + f"better_thermostat {self.device_name}: TRV {entity_id} device quirk trv02-zigbee active" ) await self.hass.services.async_call( "climate", diff --git a/custom_components/better_thermostat/model_fixes/model_quirks.py b/custom_components/better_thermostat/model_fixes/model_quirks.py index 094a050d..f6e233dd 100644 --- a/custom_components/better_thermostat/model_fixes/model_quirks.py +++ b/custom_components/better_thermostat/model_fixes/model_quirks.py @@ -12,19 +12,17 @@ async def load_model_quirks(self, model, entity_id): try: self.model_quirks = await async_import_module( - self.hass, - "custom_components.better_thermostat.model_fixes." + model, + self.hass, "custom_components.better_thermostat.model_fixes." + model ) _LOGGER.debug( "better_thermostat %s: uses quirks fixes for model %s for trv %s", - self.name, + self.device_name, model, entity_id, ) except Exception: self.model_quirks = await async_import_module( - self.hass, - "custom_components.better_thermostat.model_fixes.default", + self.hass, "custom_components.better_thermostat.model_fixes.default" ) pass @@ -45,10 +43,12 @@ def fix_local_calibration(self, entity_id, offset): self, entity_id, offset ) + _new_offset = round(_new_offset, 1) + if offset != _new_offset: _LOGGER.debug( "better_thermostat %s: %s - calibration offset model fix: %s to %s", - self.name, + self.device_name, entity_id, offset, _new_offset, @@ -74,7 +74,7 @@ def fix_target_temperature_calibration(self, entity_id, temperature): if temperature != _new_temperature: _LOGGER.debug( "better_thermostat %s: %s - temperature offset model fix: %s to %s", - self.name, + self.device_name, entity_id, temperature, _new_temperature, diff --git a/custom_components/better_thermostat/services.yaml b/custom_components/better_thermostat/services.yaml index 276bc7c7..d55dd132 100644 --- a/custom_components/better_thermostat/services.yaml +++ b/custom_components/better_thermostat/services.yaml @@ -1,52 +1,43 @@ save_current_target_temperature: - name: Save current Temperature - description: Save the current target temperature for later restoration. + name: Save target temperature + description: Save the current target temperature for later use. target: entity: domain: climate integration: better_thermostat - device: - integration: better_thermostat restore_saved_target_temperature: - name: Restore temperature - description: Restore the saved target temperature. + name: Restore target temperature + description: Restore the previously saved target temperature. target: entity: domain: climate integration: better_thermostat - device: - integration: better_thermostat reset_heating_power: name: Reset heating power - description: Reset heating power to default value. + description: Reset the heating power to its default value. target: entity: domain: climate integration: better_thermostat - device: - integration: better_thermostat set_temp_target_temperature: - name: Set the eco temperature - description: Set the target temperature to a temporay like night mode, and save the old one. + name: Set temporary (ECO) target temperature + description: Set a temporary target temperature and save the current temperature. fields: temperature: - name: Target temperature - description: The target temperature to set. - example: 18.5 + name: Temperature + description: New target temperature to set required: true - advanced: true selector: number: min: 0 max: 35 step: 0.5 mode: box + unit_of_measurement: °C target: entity: domain: climate integration: better_thermostat - device: - integration: better_thermostat diff --git a/custom_components/better_thermostat/strings.json b/custom_components/better_thermostat/strings.json index bc7b8cc3..e16f0574 100644 --- a/custom_components/better_thermostat/strings.json +++ b/custom_components/better_thermostat/strings.json @@ -10,30 +10,30 @@ "temperature_sensor": "Temperature sensor", "humidity_sensor": "Humidity sensor", "window_sensors": "Window sensor", - "off_temperature": "The outdoor temperature when the thermostat turn off", - "tolerance": "Tolerance, to prevent the thermostat from turning on and off too often.", - "window_off_delay": "Delay before the thermostat turn off when the window is opened", - "window_off_delay_after": "Delay before the thermostat turn on when the window is closed", - "outdoor_sensor": "If you have an outdoor sensor, you can use it to get the outdoor temperature", - "weather": "Your weather entity to get the outdoor temperature" + "off_temperature": "The outdoor temperature when the thermostat should turn off", + "tolerance": "Tolerance, to prevent the thermostat from turning on and off too often", + "window_off_delay": "Delay before the thermostat should turn off when the window is opened", + "window_off_delay_after": "Delay before the thermostat should turn on when the window is closed", + "outdoor_sensor": "Outdoor temperature sensor", + "weather": "Weather entity to get the outdoor temperature" } }, "advanced": { "description": "Advanced configuration {trv}\n***Info about calibration types: https://better-thermostat.org/configuration#second-step***", "data": { "protect_overheating": "Overheating protection?", - "heat_auto_swapped": "If the auto means heat for your TRV and you want to swap it", + "heat_auto_swapped": "If 'auto' means 'heat' for your TRV and you want to swap it", "child_lock": "Ignore all inputs on the TRV like a child lock", - "homaticip": "If you use HomaticIP, you should enable this to slow down the requests to prevent the duty cycle", + "homematicip": "If you use HomematicIP, you should enable this to slow down the requests to prevent the duty cycle", "valve_maintenance": "If your thermostat has no own maintenance mode, you can use this one", - "calibration": "Calibration Type", + "calibration": "Calibration type", "calibration_mode": "Calibration mode", - "no_off_system_mode": "If your TRV can't handle the off mode, you can enable this to use target temperature 5°C instead" + "no_off_system_mode": "If your TRV doesn't support the 'off' mode, enable this to use target temperature 5°C instead" }, "data_description": { - "protect_overheating": "Some TRVs don't close the valve completly when the temperature is reached. Or the radiator have a lot of rest heat. This can cause overheating. This option can prevent this.", - "calibration_mode": "The kind how the calibration should be calculated\n***Normal***: In this mode the TRV internal temperature sensor is fixed by the external temperature sensor.\n***Aggresive***: In this mode the TRV internal temperature sensor is fixed by the external temperature sensor but set much lower/higher to get a quicker boost.\n***AI Time Based***: In this mode the TRV internal temperature sensor is fixed by the external temperature sensor, but the value is calculated by a custom algorithm to improve the TRV internal algorithm.", - "calibration": "How the calibration should be applied on the TRV (Target temp or offset)\n***Target Temperature Based***: Apply the calibration to the target temperature.\n***Offset Based***: Apply the calibration to the offset." + "protect_overheating": "Some TRVs don't close the valve completely when the temperature is reached. Or the radiator has a lot of rest heat. This can cause overheating. This option can prevent this.", + "calibration_mode": "How the calibration is calculated\n***Normal***: In this mode, the TRV internal temperature sensor is fixed by the external temperature sensor.\n***Aggresive***: In this mode, the TRV internal temperature sensor is fixed by the external temperature sensor but set much lower/higher to get a quicker boost.\n***AI Time Based***: In this mode, the TRV internal temperature sensor is fixed by the external temperature sensor, but the value is calculated by a custom algorithm to improve the TRV internal algorithm.", + "calibration": "How the calibration is applied on the TRV\n***Target Temperature Based***: Apply the calibration to the target temperature.\n***Offset Based***: Apply the calibration to the temperature offset." } }, "confirm": { @@ -44,12 +44,12 @@ "error": { "failed": "Something went wrong.", "no_name": "Please enter a name.", - "no_off_mode": "You device is very special and has no off mode :(\nBetter Thermostat will use the minimal target temp instead.", + "no_off_mode": "Your device is special and has no off mode :(\nBetter Thermostat will use the minimal target temp instead.", "no_outside_temp": "You have no outside temperature sensor. Better Thermostat will use the weather entity instead." }, "abort": { - "single_instance_allowed": "Only a single Thermostat for each real is allowed.", - "no_devices_found": "No thermostat entity found, make sure you have a climate entity in your home assistant" + "single_instance_allowed": "Only a single Better Thermostat is allowed for each real thermostat.", + "no_devices_found": "No thermostat entity found, make sure you have a climate entity in your Home Assistant" } }, "options": { @@ -62,17 +62,17 @@ "temperature_sensor": "Temperature Sensor", "humidity_sensor": "Humidity sensor", "window_sensors": "Window Sensor", - "off_temperature": "The outdoor temperature when the thermostat turns off", - "tolerance": "Tolerance, to prevent the thermostat from turning on and off too often.", - "window_off_delay": "Delay before the thermostat turns off when the window is opened", - "window_off_delay_after": "Delay before the thermostat turns on when the window is closed", - "outdoor_sensor": "If you have an outdoor sensor, you can use it to get the outdoor temperature", + "off_temperature": "The outdoor temperature when the thermostat should turn off", + "tolerance": "Tolerance, to prevent the thermostat from turning on and off too often", + "window_off_delay": "Delay before the thermostat should turn off when the window is opened", + "window_off_delay_after": "Delay before the thermostat should turn on when the window is closed", + "outdoor_sensor": "Outdoor temperature sensor", + "weather": "Weather entity to get the outdoor temperature" "valve_maintenance": "If your thermostat has no own maintenance mode, you can use this one", "calibration": "The sort of calibration https://better-thermostat.org/configuration#second-step", - "weather": "Your weather entity to get the outdoor temperature", "heat_auto_swapped": "If the auto means heat for your TRV and you want to swap it", "child_lock": "Ignore all inputs on the TRV like a child lock", - "homaticip": "If you use HomaticIP, you should enable this to slow down the requests to prevent the duty cycle" + "homematicip": "If you use HomematicIP, you should enable this to slow down the requests to prevent the duty cycle" } }, "advanced": { @@ -81,17 +81,16 @@ "protect_overheating": "Overheating protection?", "heat_auto_swapped": "If the auto means heat for your TRV and you want to swap it", "child_lock": "Ignore all inputs on the TRV like a child lock", - "homaticip": "If you use HomaticIP, you should enable this to slow down the requests to prevent the duty cycle", + "homematicip": "If you use HomematicIP, you should enable this to slow down the requests to prevent the duty cycle", "valve_maintenance": "If your thermostat has no own maintenance mode, you can use this one", - "calibration": "The sort of calibration you want to use", + "calibration": "Calibration type", "calibration_mode": "Calibration mode", - "no_off_system_mode": "If your TRV can't handle the off mode, you can enable this to use target temperature 5°C instead" + "no_off_system_mode": "If your TRV doesn't support the 'off' mode, enable this to use target temperature 5°C instead" }, "data_description": { "protect_overheating": "Some TRVs don't close the valve completely when the temperature is reached. Or the radiator has a lot of rest heat. This can cause overheating. This option can prevent this.", - "calibration_mode": "The kind how the calibration should be calculated\n***Normal***: In this mode, the TRV internal temperature sensor is fixed by the external temperature sensor.\n***Aggresive***: In this mode, the TRV internal temperature sensor is fixed by the external temperature sensor but set much lower/higher to get a quicker boost.\n***AI Time Based***: In this mode, the TRV internal temperature sensor is fixed by the external temperature sensor, but a custom algorithm calculates the value to improve the TRV internal algorithm.", - "calibration": "How the calibration should be applied on the TRV (Target temp or offset)\n***Target Temperature Based***: Apply the calibration to the target temperature.\n***Offset Based***: Apply the calibration to the offset." - } + "calibration_mode": "How the calibration is calculated\n***Normal***: In this mode, the TRV internal temperature sensor is fixed by the external temperature sensor.\n***Aggresive***: In this mode, the TRV internal temperature sensor is fixed by the external temperature sensor but set much lower/higher to get a quicker boost.\n***AI Time Based***: In this mode, the TRV internal temperature sensor is fixed by the external temperature sensor, but the value is calculated by a custom algorithm to improve the TRV internal algorithm.", + "calibration": "How the calibration is applied on the TRV\n***Target Temperature Based***: Apply the calibration to the target temperature.\n***Offset Based***: Apply the calibration to the temperature offset." } } } }, @@ -123,7 +122,7 @@ }, "set_temp_target_temperature": { "name": "Set eco temperature", - "description": "Set the target temperature to a temporay like night mode, and save the old one." + "description": "Set the target temperature to a temporary like night mode, and save the old one." } } } diff --git a/custom_components/better_thermostat/translations/da.json b/custom_components/better_thermostat/translations/da.json index 6b4935e1..1f9c5777 100644 --- a/custom_components/better_thermostat/translations/da.json +++ b/custom_components/better_thermostat/translations/da.json @@ -7,30 +7,33 @@ "data": { "name": "Navn", "thermostat": "Den rigtige termostat", + "cooler": "Køleenhed (valgfri)", "temperature_sensor": "Temperatur måler", "humidity_sensor": "Fugtighedssensor", "window_sensors": "Vinduessensor", "off_temperature": "Udetemperaturen, når termostaten slukker", + "tolerance": "Tolerancen for at forhindre, at termostaten tænder og slukker for ofte.", "window_off_delay": "Forsinkelse, før termostaten slukker, når vinduet er åbent, og tændt, når vinduet er lukket", + "window_off_delay_after": "Forsinkelse, før termostaten tænder, når vinduet er lukket", "outdoor_sensor": "Hvis du har en udeføler, kan du bruge den til at få udetemperaturen", "weather": "Din vejrentitet for at få udendørstemperaturen" } }, "advanced": { - "description": "Avanceret konfiguration\n\n**Info om kalibreringstyper: https://better-thermostat.org/configuration#second-step** ", + "description": "Avanceret konfiguration {trv}\n***Info om kalibreringstyper: https://better-thermostat.org/configuration#second-step*** ", "data": { + "protect_overheating": "Overophedningsbeskyttelse?", "heat_auto_swapped": "Hvis auto betyder varme for din TRV og du vil bytte det", "child_lock": "Ignorer alle input på TRV som en børnesikring", - "homaticip": "Hvis du bruger HomaticIP, bør du aktivere dette for at bremse anmodningerne for at forhindre arbejdscyklus", + "homematicip": "Hvis du bruger HomematicIP, bør du aktivere dette for at bremse anmodningerne for at forhindre arbejdscyklus", "valve_maintenance": "Hvis din termostat ikke har nogen egen vedligeholdelsestilstand, kan du bruge denne", "calibration": "Den slags kalibrering du vil bruge", - "no_off_system_mode": "Hvis din TRV ikke kan håndtere den slukkede tilstand, kan du aktivere denne for at bruge måltemperatur 5°C i stedet", "calibration_mode": "Kalibreringstilstand", - "protect_overheating": "Overophedningsbeskyttelse?" + "no_off_system_mode": "Hvis din TRV ikke kan håndtere den slukkede tilstand, kan du aktivere denne for at bruge måltemperatur 5°C i stedet" }, "data_description": { "protect_overheating": "Nogle TRV'er lukker ikke ventilen helt, når temperaturen er nået. Eller radiatoren har meget hvilevarme. Dette kan forårsage overophedning. Denne mulighed kan forhindre dette.", - "calibration_mode": "Den slags, hvordan kalibreringen skal beregnes\n***Normal***: I denne tilstand er TRV's interne temperaturføler fastgjort af den eksterne temperaturføler.\n***Aggressiv***: I denne tilstand er TRV's interne temperaturføler fikseret af den eksterne temperaturføler, men indstillet meget lavere/højere for at få et hurtigere løft.\n***AI-tidsbaseret***: I denne tilstand er TRV's interne temperatursensor fastsat af den eksterne temperatursensor, men værdien beregnes af en brugerdefineret algoritme for at forbedre TRV's interne algoritme.", + "calibration_mode": "Hvordan kalibreringen skal beregnes\n***Normal***: I denne tilstand er TRV's interne temperaturføler fastgjort af den eksterne temperaturføler.\n***Aggressiv***: I denne tilstand er TRV's interne temperaturføler fikseret af den eksterne temperaturføler, men indstillet meget lavere/højere for at få et hurtigere løft.\n***AI-tidsbaseret***: I denne tilstand er TRV's interne temperatursensor fastsat af den eksterne temperatursensor, men værdien beregnes af en brugerdefineret algoritme for at forbedre TRV's interne algoritme.", "calibration": "Hvordan kalibreringen skal anvendes på TRV (Target temp or offset)\n***Måltemperaturbaseret***: Anvend kalibreringen til måltemperaturen.\n***Offset-baseret***: Anvend kalibreringen til offset." } }, @@ -40,10 +43,10 @@ } }, "error": { - "no_outside_temp": "Du har ingen udetemperaturføler. Bedre termostat vil bruge vejret i stedet.", - "failed": "noget gik galt.", + "failed": "Noget gik galt.", "no_name": "Indtast venligst et navn.", - "no_off_mode": "Din enhed er meget speciel og har ingen slukket tilstand :(\nDet virker alligevel, men du skal oprette en automatisering, der passer til dine specialer baseret på enhedsbegivenhederne" + "no_off_mode": "Din enhed er meget speciel og har ingen slukket tilstand :(\nDet virker alligevel, men du skal oprette en automatisering, der passer til dine specialer baseret på enhedsbegivenhederne", + "no_outside_temp": "Du har ingen udetemperaturføler. Better Thermostat vil bruge vejret i stedet." }, "abort": { "single_instance_allowed": "Kun en enkelt termostat for hver virkelige er tilladt.", @@ -55,37 +58,39 @@ "user": { "description": "Opdater dine Better Thermostat indstillinger", "data": { + "name": "Navn", + "thermostat": "Den rigtige termostat", "temperature_sensor": "Temperatur måler", "humidity_sensor": "Fugtighedssensor", "window_sensors": "Vinduessensor", "off_temperature": "Udendørstemperaturen, når termostaten skal slukker", + "tolerance": "Tolerancen for at forhindre, at termostaten tænder og slukker for ofte.", "window_off_delay": "Forsinkelse, før termostaten slukker, når vinduet er åbent, og tændt, når vinduet er lukket", + "window_off_delay_after": "Forsinkelse, før termostaten tænder, når vinduet er lukket", "outdoor_sensor": "Har du en udendørsføler, kan du bruge den til at få udetemperaturen", "valve_maintenance": "Hvis din termostat ikke har nogen egen vedligeholdelsestilstand, kan du bruge denne", "calibration": "Den slags kalibrering https://better-thermostat.org/configuration#second-step", "weather": "Din vejrentitet for at få udendørstemperaturen", "heat_auto_swapped": "Hvis auto betyder varme til din TRV, og du ønsker at bytte den", "child_lock": "Ignorer alle input på TRV som en børnesikring", - "homaticip": "Hvis du bruger HomaticIP, bør du aktivere dette for at bremse anmodningerne for at forhindre arbejdscyklus", - "name": "Navn", - "thermostat": "Den rigtige termostat" + "homaticip": "Hvis du bruger HomematicIP, bør du aktivere dette for at bremse anmodningerne for at forhindre arbejdscyklus" } }, "advanced": { - "description": "Avanceret konfiguration\n\n**Info om kalibreringstyper: https://better-thermostat.org/configuration#second-step** ", + "description": "Avanceret konfiguration {trv}\n***Info om kalibreringstyper: https://better-thermostat.org/configuration#second-step*** ", "data": { + "protect_overheating": "Overophedningsbeskyttelse?", "heat_auto_swapped": "Hvis auto betyder varme for din TRV og du vil bytte det", "child_lock": "Ignorer alle input på TRV som en børnesikring", - "homaticip": "Hvis du bruger HomaticIP, bør du aktivere dette for at bremse anmodningerne for at forhindre arbejdscyklus", + "homematicip": "Hvis du bruger HomematicIP, bør du aktivere dette for at bremse anmodningerne for at forhindre arbejdscyklus", "valve_maintenance": "Hvis din termostat ikke har nogen egen vedligeholdelsestilstand, kan du bruge denne", "calibration": "Den slags kalibrering du vil bruge", - "protect_overheating": "Overophedningsbeskyttelse?", "calibration_mode": "Kalibreringstilstand", "no_off_system_mode": "Hvis din TRV ikke kan håndtere den slukkede tilstand, kan du aktivere denne for at bruge måltemperatur 5°C i stedet" }, "data_description": { "protect_overheating": "Nogle TRV'er lukker ikke ventilen helt, når temperaturen er nået. Eller radiatoren har meget hvilevarme. Dette kan forårsage overophedning. Denne mulighed kan forhindre dette.", - "calibration_mode": "Den slags, hvordan kalibreringen skal beregnes\n***Normal***: I denne tilstand er TRV's interne temperaturføler fikseret af den eksterne temperaturføler.\n***Aggressiv***: I denne tilstand er TRV's interne temperaturføler fikseret af den eksterne temperaturføler, men indstillet meget lavere/højere for at få et hurtigere løft.\n***AI-tidsbaseret***: I denne tilstand er TRV's interne temperatursensor fastsat af den eksterne temperatursensor, men værdien beregnes af en brugerdefineret algoritme for at forbedre TRV's interne algoritme.", + "calibration_mode": "Hvordan kalibreringen skal beregnes\n***Normal***: I denne tilstand er TRV's interne temperaturføler fikseret af den eksterne temperaturføler.\n***Aggressiv***: I denne tilstand er TRV's interne temperaturføler fikseret af den eksterne temperaturføler, men indstillet meget lavere/højere for at få et hurtigere løft.\n***AI-tidsbaseret***: I denne tilstand er TRV's interne temperatursensor fastsat af den eksterne temperatursensor, men værdien beregnes af en brugerdefineret algoritme for at forbedre TRV's interne algoritme.", "calibration": "Hvordan kalibreringen skal anvendes på TRV (Target temp or offset)\n***Måltemperaturbaseret***: Anvend kalibreringen til måltemperaturen.\n***Offset-baseret***: Anvend kalibreringen til offset." } } @@ -103,5 +108,23 @@ } } } + }, + "services": { + "save_current_target_temperature": { + "name": "Gem nuværende temperatur", + "description": "Gem den nuværende måltemperatur til senere gendannelse." + }, + "restore_saved_target_temperature": { + "name": "Gendan temperatur", + "description": "Gendan den gemte måltemperatur." + }, + "reset_heating_power": { + "name": "Nulstil opvarmningskraft", + "description": "Nulstil opvarmningskraft til standardværdi." + }, + "set_temp_target_temperature": { + "name": "Indstil eco-temperatur", + "description": "Indstil måltemperaturen til en midlertidig som nattilstand, og gem den gamle." + } } -} \ No newline at end of file +} diff --git a/custom_components/better_thermostat/translations/de.json b/custom_components/better_thermostat/translations/de.json index c34bc9a5..58296033 100644 --- a/custom_components/better_thermostat/translations/de.json +++ b/custom_components/better_thermostat/translations/de.json @@ -25,7 +25,7 @@ "protect_overheating": "Überhitzung verhindern?", "heat_auto_swapped": "Tauscht die Modi auto und heat, falls diese bei dem realen Thermostat vertauscht sind.", "child_lock": "Ignoriere alle manuellen Einstellungen am realen Thermostat (Kindersicherung).", - "homaticip": "Wenn du HomaticIP nutzt, solltest du diese Option aktivieren, um die Funk-Übertragung zu reduzieren.", + "homematicip": "Wenn du HomematicIP nutzt, solltest du diese Option aktivieren, um die Funk-Übertragung zu reduzieren.", "valve_maintenance": "Soll BT die Wartung des Thermostats übernehmen?", "calibration": "Kalibrierungstyp", "calibration_mode": "Kalibrierungsmodus", @@ -71,7 +71,7 @@ "weather": "Die Wetter-Entität für die Außentemperatur.", "valve_maintenance": "Wenn Ihr Thermostat keinen eigenen Wartungsmodus hat, können Sie diesen verwenden.", "child_lock": "Ignorieren Sie alle Eingaben am TRV wie eine Kindersicherung.", - "homaticip": "Wenn Sie HomaticIP verwenden, sollten Sie dies aktivieren, um die Anfragen zu verlangsamen und den Duty Cycle zu verhindern.", + "homematicip": "Wenn Sie HomematicIP verwenden, sollten Sie dies aktivieren, um die Anfragen zu verlangsamen und den Duty Cycle zu verhindern.", "heat_auto_swapped": "Wenn das Auto Wärme für Ihr TRV bedeutet und Sie es austauschen möchten.", "calibration": "Die Art der Kalibrierung https://better-thermostat.org/configuration#second-step" } @@ -82,7 +82,7 @@ "protect_overheating": "Überhitzung verhindern?", "heat_auto_swapped": "Tauscht die Modi auto und heat, falls diese bei dem realen Thermostat vertauscht sind.", "child_lock": "Ignoriere alle manuellen Einstellungen am realen Thermostat (Kindersicherung).", - "homaticip": "Wenn du HomaticIP nutzt, solltest du diese Option aktivieren, um die Funk-Übertragung zu reduzieren.", + "homematicip": "Wenn du HomematicIP nutzt, solltest du diese Option aktivieren, um die Funk-Übertragung zu reduzieren.", "valve_maintenance": "Soll BT die Wartung des Thermostats übernehmen?", "calibration": "Kalibrierungstyp", "calibration_mode": "Kalibrierungsmodus", diff --git a/custom_components/better_thermostat/translations/en.json b/custom_components/better_thermostat/translations/en.json index c20b3fbe..2d9fcebf 100644 --- a/custom_components/better_thermostat/translations/en.json +++ b/custom_components/better_thermostat/translations/en.json @@ -25,7 +25,7 @@ "protect_overheating": "Overheating protection?", "heat_auto_swapped": "If the auto means heat for your TRV and you want to swap it", "child_lock": "Ignore all inputs on the TRV like a child lock", - "homaticip": "If you use HomaticIP, you should enable this to slow down the requests to prevent the duty cycle", + "homematicip": "If you use HomematicIP, you should enable this to slow down the requests to prevent the duty cycle", "valve_maintenance": "If your thermostat has no own maintenance mode, you can use this one", "calibration": "Calibration Type", "calibration_mode": "Calibration mode", @@ -73,7 +73,7 @@ "weather": "Your weather entity to get the outdoor temperature", "heat_auto_swapped": "If the auto means heat for your TRV and you want to swap it", "child_lock": "Ignore all inputs on the TRV like a child lock", - "homaticip": "If you use HomaticIP, you should enable this to slow down the requests to prevent the duty cycle" + "homematicip": "If you use HomematicIP, you should enable this to slow down the requests to prevent the duty cycle" } }, "advanced": { @@ -82,7 +82,7 @@ "protect_overheating": "Overheating protection?", "heat_auto_swapped": "If the auto means heat for your TRV and you want to swap it", "child_lock": "Ignore all inputs on the TRV like a child lock", - "homaticip": "If you use HomaticIP, you should enable this to slow down the requests to prevent the duty cycle", + "homematicip": "If you use HomematicIP, you should enable this to slow down the requests to prevent the duty cycle", "valve_maintenance": "If your thermostat has no own maintenance mode, you can use this one", "calibration": "The sort of calibration you want to use", "calibration_mode": "Calibration mode", diff --git a/custom_components/better_thermostat/translations/fr.json b/custom_components/better_thermostat/translations/fr.json index ea99cd87..e7b19dda 100644 --- a/custom_components/better_thermostat/translations/fr.json +++ b/custom_components/better_thermostat/translations/fr.json @@ -25,7 +25,7 @@ "protect_overheating": "Protection contre la surchauffe ?", "heat_auto_swapped": "Si le mode automatique signifie chauffage pour votre TRV et que vous voulez le permuter", "child_lock": "Ignorer toutes les entrées sur le TRV comme une sécurité enfant", - "homaticip": "Si vous utilisez HomaticIP, vous devriez l'activer pour ralentir les demandes et éviter le cycle de fonctionnement excessif", + "homematicip": "Si vous utilisez HomematicIP, vous devriez l'activer pour ralentir les demandes et éviter le cycle de fonctionnement excessif", "valve_maintenance": "Si votre thermostat n'a pas de mode maintenance propre, vous pouvez utiliser celui-ci", "calibration": "Type de calibrage", "calibration_mode": "Mode de calibrage", @@ -73,7 +73,7 @@ "weather": "Votre entité météo pour obtenir la température extérieure", "heat_auto_swapped": "Si le mode automatique signifie chauffage pour votre TRV et que vous voulez le permuter", "child_lock": "Ignorer toutes les entrées sur le TRV comme une sécurité enfant", - "homaticip": "Si vous utilisez HomaticIP, vous devriez l'activer pour ralentir les demandes et éviter le cycle de fonctionnement excessif" + "homematicip": "Si vous utilisez HomematicIP, vous devriez l'activer pour ralentir les demandes et éviter le cycle de fonctionnement excessif" } }, "advanced": { @@ -82,7 +82,7 @@ "protect_overheating": "Protection contre la surchauffe ?", "heat_auto_swapped": "Si le mode automatique signifie chauffage pour votre TRV et que vous voulez le permuter", "child_lock": "Ignorer toutes les entrées sur le TRV comme une sécurité enfant", - "homaticip": "Si vous utilisez HomaticIP, vous devriez l'activer pour ralentir les demandes et éviter le cycle de fonctionnement excessif", + "homematicip": "Si vous utilisez HomematicIP, vous devriez l'activer pour ralentir les demandes et éviter le cycle de fonctionnement excessif", "valve_maintenance": "Si votre thermostat n'a pas de mode maintenance propre, vous pouvez utiliser celui-ci", "calibration": "Le type de calibrage que vous souhaitez utiliser", "calibration_mode": "Mode de calibrage", diff --git a/custom_components/better_thermostat/translations/pl.json b/custom_components/better_thermostat/translations/pl.json index 0bf2822d..4f1a59aa 100644 --- a/custom_components/better_thermostat/translations/pl.json +++ b/custom_components/better_thermostat/translations/pl.json @@ -17,11 +17,11 @@ } }, "advanced": { - "description": "Zaawansowana konfiguracja\n\n**Informacja o typach kalibracji: https://better-thermostat.org/configuration#second-step** ", + "description": "Zaawansowana konfiguracja {trv}\n***Informacja o typach kalibracji: https://better-thermostat.org/configuration#second-step***", "data": { "heat_auto_swapped": "Jeżeli tryb auto oznacza grzanie dla Twojego TRV i chcesz to zmienić", "child_lock": "Ignoruj wszystkie wejścia w TRV jak np. Blokada dziecięca", - "homaticip": "Jeżeli używasz HomaticIP, powinieneś włączyć tę opcję, żeby spowolnić żądania cyklu pracy", + "homematicip": "Jeżeli używasz HomematicIP, powinieneś włączyć tę opcję, żeby spowolnić żądania cyklu pracy", "valve_maintenance": "Jeżeli Twój termostat nie ma trybu konserwacji, możesz użyć tej opcji.", "calibration": "Rodzaj kalibracji, której chcesz użyć", "no_off_system_mode": "Jeśli Twój TRV nie obsługuje trybu wyłączenia, możesz go włączyć, aby zamiast tego używać temperatury docelowej 5°C", @@ -66,17 +66,17 @@ "weather": "Twoja jednostka pogodowa, aby uzyskać temperaturę zewnętrzną", "heat_auto_swapped": "Jeżeli tryb auto oznacza grzanie dla Twojego TRV i chcesz to zmienić", "child_lock": "Ignoruj wszystkie wejścia w TRV jak np. Blokada dziecięca", - "homaticip": "Jeżeli używasz HomaticIP, powinienieś włączyć tę opcję żeby spowolnić żądania", + "homematicip": "Jeżeli używasz HomematicIP, powinienieś włączyć tę opcję żeby spowolnić żądania", "name": "Nazwa", "thermostat": "Prawdziwy termostat" } }, "advanced": { - "description": "Zaawansowana konfiguracja**Informacja o typach kalibracji: https://better-thermostat.org/configuration#second-step** ", + "description": "Zaawansowana konfiguracja {trv}\n***Informacja o typach kalibracji: https://better-thermostat.org/configuration#second-step***", "data": { "heat_auto_swapped": "Jeżeli tryb auto oznacza grzanie dla Twojego TRV i chcesz to zmienić", "child_lock": "Ignoruj wszystkie wejścia w TRV jak np. Blokada dziecięca", - "homaticip": "Jeżeli używasz HomaticIP, powinieneś włączyć tę opcję, żeby spowolnić żądania spowolnienia cyklu pracy", + "homematicip": "Jeżeli używasz HomematicIP, powinieneś włączyć tę opcję, żeby spowolnić żądania spowolnienia cyklu pracy", "valve_maintenance": "Jeżeli Twój termostat nie ma trybu konserwacji, możesz użyć tej opcji.", "calibration": "Rodzaj kalibracji, której chcesz użyć", "protect_overheating": "Zabezpieczenie przed przegrzaniem?", diff --git a/custom_components/better_thermostat/translations/ru.json b/custom_components/better_thermostat/translations/ru.json index 9d173760..3cfbd118 100644 --- a/custom_components/better_thermostat/translations/ru.json +++ b/custom_components/better_thermostat/translations/ru.json @@ -25,7 +25,7 @@ "protect_overheating": "Защита от перегрева?", "heat_auto_swapped": "Если автомат означает обогрев вашего термостата, и вы хотите его поменять", "child_lock": "Игнорировать все входы на термостате, как блокировку от детей.", - "homaticip": "Если вы используете HomaticIP, вам следует включить это, чтобы замедлить запросы и предотвратить рабочий цикл.", + "homematicip": "Если вы используете HomematicIP, вам следует включить это, чтобы замедлить запросы и предотвратить рабочий цикл.", "valve_maintenance": "Если у вашего термостата нет собственного режима обслуживания, вы можете использовать этот.", "calibration": "Тип калибровки", "calibration_mode": "Режим калибровки", @@ -73,7 +73,7 @@ "weather": "Датчик температуры наружного воздуха", "heat_auto_swapped": "Если автомат означает обогрев вашего термостата, и вы хотите его поменять", "child_lock": "Игнорировать все входы на термостате, как блокировку от детей.", - "homaticip": "Если вы используете HomaticIP, вам следует включить это, чтобы замедлить запросы и предотвратить рабочий цикл." + "homematicip": "Если вы используете HomematicIP, вам следует включить это, чтобы замедлить запросы и предотвратить рабочий цикл." } }, "advanced": { @@ -82,7 +82,7 @@ "protect_overheating": "Защита от перегрева?", "heat_auto_swapped": "Если стоит функция auto, это означает нагрев вашего термостата и вы хотите его заменить", "child_lock": "Игнорировать все входы на термостате, как блокировку от детей.", - "homaticip": "Если вы используете HomaticIP, вам следует включить это, чтобы замедлить запросы и предотвратить рабочий цикл.", + "homematicip": "Если вы используете HomematicIP, вам следует включить это, чтобы замедлить запросы и предотвратить рабочий цикл.", "valve_maintenance": "Если у вашего термостата нет собственного режима обслуживания, вы можете использовать этот.", "calibration": "Тип калибровки, которую вы хотите использовать", "calibration_mode": "Режим калибровки", diff --git a/custom_components/better_thermostat/translations/sk.json b/custom_components/better_thermostat/translations/sk.json index 1c7e3782..d079e433 100644 --- a/custom_components/better_thermostat/translations/sk.json +++ b/custom_components/better_thermostat/translations/sk.json @@ -25,7 +25,7 @@ "protect_overheating": "Ochrana proti prehriatiu?", "heat_auto_swapped": "Ak auto znamená teplo pre váš TRV a chcete ho vymeniť", "child_lock": "Ignorovať všetky vstupy na TRV ako detský zámok", - "homaticip": "Ak používate HomaticIP, mali by ste túto funkciu povoliť, aby ste spomalili požiadavky a zabránili tak pracovnému cyklu", + "homematicip": "Ak používate HomematicIP, mali by ste túto funkciu povoliť, aby ste spomalili požiadavky a zabránili tak pracovnému cyklu", "valve_maintenance": "Ak váš termostat nemá vlastný režim údržby, môžete použiť tento režim", "calibration": "Typ kalibrácie", "calibration_mode": "Režim kalibrácie", @@ -73,7 +73,7 @@ "weather": "Váš meteorologický subjekt na zistenie vonkajšej teploty", "heat_auto_swapped": "Ak auto znamená teplo pre váš TRV a chcete ho vymeniť", "child_lock": "Ignorovať všetky vstupy na TRV ako detský zámok", - "homaticip": "Ak používate HomaticIP, mali by ste túto funkciu povoliť, aby ste spomalili požiadavky a zabránili tak pracovnému cyklu" + "homematicip": "Ak používate HomematicIP, mali by ste túto funkciu povoliť, aby ste spomalili požiadavky a zabránili tak pracovnému cyklu" } }, "advanced": { @@ -82,7 +82,7 @@ "protect_overheating": "Ochrana proti prehriatiu?", "heat_auto_swapped": "Ak auto znamená teplo pre váš TRV a chcete ho vymeniť", "child_lock": "Ignorovať všetky vstupy na TRV ako detský zámok", - "homaticip": "Ak používate HomaticIP, mali by ste túto funkciu povoliť, aby ste spomalili požiadavky a zabránili tak pracovnému cyklu", + "homematicip": "Ak používate HomematicIP, mali by ste túto funkciu povoliť, aby ste spomalili požiadavky a zabránili tak pracovnému cyklu", "valve_maintenance": "Ak váš termostat nemá vlastný režim údržby, môžete použiť tento režim", "calibration": "Druh kalibrácie, ktorý chcete použiť", "calibration_mode": "Režim kalibrácie", diff --git a/custom_components/better_thermostat/utils/const.py b/custom_components/better_thermostat/utils/const.py index 0c8238d0..cd319a1c 100644 --- a/custom_components/better_thermostat/utils/const.py +++ b/custom_components/better_thermostat/utils/const.py @@ -49,7 +49,7 @@ CONF_CALIBRATION_MODE = "calibration_mode" CONF_HEAT_AUTO_SWAPPED = "heat_auto_swapped" CONF_MODEL = "model" -CONF_HOMATICIP = "homaticip" +CONF_HOMEMATICIP = "homematicip" CONF_INTEGRATION = "integration" CONF_NO_SYSTEM_MODE_OFF = "no_off_system_mode" CONF_TOLERANCE = "tolerance" diff --git a/custom_components/better_thermostat/utils/controlling.py b/custom_components/better_thermostat/utils/controlling.py index 95eafc12..693f6e4d 100644 --- a/custom_components/better_thermostat/utils/controlling.py +++ b/custom_components/better_thermostat/utils/controlling.py @@ -28,6 +28,17 @@ _LOGGER = logging.getLogger(__name__) +class TaskManager: + def __init__(self): + self.tasks = set() + + def create_task(self, coro): + task = asyncio.create_task(coro) + self.tasks.add(task) + task.add_done_callback(self.tasks.discard) + return task + + async def control_queue(self): """The accutal control loop. Parameters @@ -39,6 +50,9 @@ async def control_queue(self): ------- None """ + if not hasattr(self, "task_manager"): + self.task_manager = TaskManager() + while True: if self.ignore_states or self.startup_running: await asyncio.sleep(1) @@ -56,7 +70,7 @@ async def control_queue(self): except Exception: _LOGGER.exception( "better_thermostat %s: ERROR controlling: %s", - self.name, + self.device_name, trv, ) result = False @@ -87,13 +101,18 @@ async def control_trv(self, heater_entity_id=None): ------- None """ + if not hasattr(self, "task_manager"): + self.task_manager = TaskManager() + async with self._temp_lock: self.real_trvs[heater_entity_id]["ignore_trv_states"] = True await update_hvac_action(self) await self.calculate_heating_power() _trv = self.hass.states.get(heater_entity_id) _current_set_temperature = convert_to_float( - str(_trv.attributes.get("temperature", None)), self.name, "controlling()" + str(_trv.attributes.get("temperature", None)), + self.device_name, + "controlling()", ) _remapped_states = convert_outbound_states( @@ -101,7 +120,7 @@ async def control_trv(self, heater_entity_id=None): ) if not isinstance(_remapped_states, dict): _LOGGER.debug( - f"better_thermostat {self.name}: ERROR {heater_entity_id} {_remapped_states}" + f"better_thermostat {self.device_name}: ERROR {heater_entity_id} {_remapped_states}" ) await asyncio.sleep(10) self.ignore_states = False @@ -141,7 +160,7 @@ async def control_trv(self, heater_entity_id=None): context=self.context, ) elif ( - self.cur_temp < self.bt_target_cooltemp - self.tolerance + self.cur_temp <= (self.bt_target_cooltemp - self.tolerance) and _new_hvac_mode is not HVACMode.OFF ): await self.hass.services.async_call( @@ -196,7 +215,7 @@ async def control_trv(self, heater_entity_id=None): if _no_off_system_mode is True and _new_hvac_mode == HVACMode.OFF: _min_temp = self.real_trvs[heater_entity_id]["min_temp"] _LOGGER.debug( - f"better_thermostat {self.name}: sending {_min_temp}°C to the TRV because this device has no system mode off and heater should be off" + f"better_thermostat {self.device_name}: sending {_min_temp}°C to the TRV because this device has no system mode off and heater should be off" ) _temperature = _min_temp @@ -210,7 +229,7 @@ async def control_trv(self, heater_entity_id=None): ) ): _LOGGER.debug( - f"better_thermostat {self.name}: TO TRV set_hvac_mode: {heater_entity_id} from: {_trv.state} to: {_new_hvac_mode}" + f"better_thermostat {self.device_name}: TO TRV set_hvac_mode: {heater_entity_id} from: {_trv.state} to: {_new_hvac_mode}" ) self.real_trvs[heater_entity_id]["last_hvac_mode"] = _new_hvac_mode _tvr_has_quirk = await override_set_hvac_mode( @@ -220,7 +239,7 @@ async def control_trv(self, heater_entity_id=None): await set_hvac_mode(self, heater_entity_id, _new_hvac_mode) if self.real_trvs[heater_entity_id]["system_mode_received"] is True: self.real_trvs[heater_entity_id]["system_mode_received"] = False - asyncio.create_task(check_system_mode(self, heater_entity_id)) + self.task_manager.create_task(check_system_mode(self, heater_entity_id)) # set new calibration offset if ( @@ -233,7 +252,7 @@ async def control_trv(self, heater_entity_id=None): if _current_calibration_s is None: _LOGGER.error( "better_thermostat %s: calibration fatal error %s", - self.name, + self.device_name, heater_entity_id, ) # this should not be before, set_hvac_mode (because if it fails, the new hvac mode will never be sent) @@ -242,7 +261,7 @@ async def control_trv(self, heater_entity_id=None): return True _current_calibration = convert_to_float( - str(_current_calibration_s), self.name, "controlling()" + str(_current_calibration_s), self.device_name, "controlling()" ) _calibration = float(str(_calibration)) @@ -255,7 +274,7 @@ async def control_trv(self, heater_entity_id=None): "calibration_received" ] is True and float(_old_calibration) != float(_calibration): _LOGGER.debug( - f"better_thermostat {self.name}: TO TRV set_local_temperature_calibration: {heater_entity_id} from: {_old_calibration} to: {_calibration}" + f"better_thermostat {self.device_name}: TO TRV set_local_temperature_calibration: {heater_entity_id} from: {_old_calibration} to: {_calibration}" ) await set_offset(self, heater_entity_id, _calibration) self.real_trvs[heater_entity_id]["calibration_received"] = False @@ -267,7 +286,7 @@ async def control_trv(self, heater_entity_id=None): if _temperature != _current_set_temperature: old = self.real_trvs[heater_entity_id].get("last_temperature", "?") _LOGGER.debug( - f"better_thermostat {self.name}: TO TRV set_temperature: {heater_entity_id} from: {old} to: {_temperature}" + f"better_thermostat {self.device_name}: TO TRV set_temperature: {heater_entity_id} from: {old} to: {_temperature}" ) self.real_trvs[heater_entity_id]["last_temperature"] = _temperature _tvr_has_quirk = await override_set_temperature( @@ -277,7 +296,7 @@ async def control_trv(self, heater_entity_id=None): await set_temperature(self, heater_entity_id, _temperature) if self.real_trvs[heater_entity_id]["target_temp_received"] is True: self.real_trvs[heater_entity_id]["target_temp_received"] = False - asyncio.create_task( + self.task_manager.create_task( check_target_temperature(self, heater_entity_id) ) @@ -297,13 +316,13 @@ def handle_window_open(self, _remapped_states): _hvac_mode_send = HVACMode.OFF self.last_window_state = True _LOGGER.debug( - f"better_thermostat {self.name}: control_trv: window is open or status of window is unknown, setting window open" + f"better_thermostat {self.device_name}: control_trv: window is open or status of window is unknown, setting window open" ) elif self.window_open is False and self.last_window_state is True: _hvac_mode_send = self.last_main_hvac_mode self.last_window_state = False _LOGGER.debug( - f"better_thermostat {self.name}: control_trv: window is closed, setting window closed restoring mode: {_hvac_mode_send}" + f"better_thermostat {self.device_name}: control_trv: window is closed, setting window closed restoring mode: {_hvac_mode_send}" ) # Force off on window open @@ -320,7 +339,7 @@ async def check_system_mode(self, heater_entity_id=None): while _real_trv["hvac_mode"] != _real_trv["last_hvac_mode"]: if _timeout > 360: _LOGGER.debug( - f"better_thermostat {self.name}: {heater_entity_id} the real TRV did not respond to the system mode change" + f"better_thermostat {self.device_name}: {heater_entity_id} the real TRV did not respond to the system mode change" ) _timeout = 0 break @@ -342,12 +361,12 @@ async def check_target_temperature(self, heater_entity_id=None): "temperature", None ) ), - self.name, + self.device_name, "check_target_temperature()", ) if _timeout == 0: _LOGGER.debug( - f"better_thermostat {self.name}: {heater_entity_id} / check_target_temp / _last: {_real_trv['last_temperature']} - _current: {_current_set_temperature}" + f"better_thermostat {self.device_name}: {heater_entity_id} / check_target_temp / _last: {_real_trv['last_temperature']} - _current: {_current_set_temperature}" ) if ( _current_set_temperature is None @@ -357,7 +376,7 @@ async def check_target_temperature(self, heater_entity_id=None): break if _timeout > 360: _LOGGER.debug( - f"better_thermostat {self.name}: {heater_entity_id} the real TRV did not respond to the target temperature change" + f"better_thermostat {self.device_name}: {heater_entity_id} the real TRV did not respond to the target temperature change" ) _timeout = 0 break diff --git a/custom_components/better_thermostat/utils/helpers.py b/custom_components/better_thermostat/utils/helpers.py index 364642c7..4ed99ef4 100644 --- a/custom_components/better_thermostat/utils/helpers.py +++ b/custom_components/better_thermostat/utils/helpers.py @@ -2,7 +2,9 @@ import re import logging +import math from datetime import datetime +from enum import Enum from typing import Union from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_registry import async_entries_for_config_entry @@ -46,20 +48,27 @@ def mode_remap(self, entity_id, hvac_mode: str, inbound: bool = False) -> str: ) if _heat_auto_swapped: - if hvac_mode == HVACMode.HEAT and inbound is False: + if hvac_mode == HVACMode.HEAT and not inbound: return HVACMode.AUTO - elif hvac_mode == HVACMode.AUTO and inbound is True: + if hvac_mode == HVACMode.AUTO and inbound: return HVACMode.HEAT - else: - return hvac_mode - else: - if hvac_mode != HVACMode.AUTO: - return hvac_mode - else: - _LOGGER.error( - f"better_thermostat {self.name}: {entity_id} HVAC mode {hvac_mode} is not supported by this device, is it possible that you forgot to set the heat auto swapped option?" - ) - return HVACMode.OFF + return hvac_mode + + trv_modes = self.real_trvs[entity_id]["hvac_modes"] + if not HVACMode.HEAT in trv_modes and HVACMode.HEAT_COOL in trv_modes: + # entity only supports HEAT_COOL, but not HEAT - need to translate + if not inbound and hvac_mode = HVACMode.HEAT: + return HVACMode.HEAT_COOL + if inbound and hvac_mode = HVACMode.HEAT_COOL: + return HVACMode.HEAT + + if hvac_mode != HVACMode.AUTO: + return hvac_mode + + _LOGGER.error( + f"better_thermostat {self.device_name}: {entity_id} HVAC mode {hvac_mode} is not supported by this device, is it possible that you forgot to set the heat auto swapped option?" + ) + return HVACMode.OFF def heating_power_valve_position(self, entity_id): @@ -71,13 +80,13 @@ def heating_power_valve_position(self, entity_id): valve_pos = 1.0 _LOGGER.debug( - f"better_thermostat {self.name}: {entity_id} / heating_power_valve_position - temp diff: {round(_temp_diff, 1)} - heating power: {round(self.heating_power, 4)} - expected valve position: {round(valve_pos * 100)}%" + f"better_thermostat {self.device_name}: {entity_id} / heating_power_valve_position - temp diff: {round(_temp_diff, 1)} - heating power: {round(self.heating_power, 4)} - expected valve position: {round(valve_pos * 100)}%" ) return valve_pos def convert_to_float( - value: Union[str, int, float], instance_name: str, context: str + value: Union[str, float], instance_name: str, context: str ) -> Union[float, None]: """Convert value to float or print error message. @@ -97,74 +106,35 @@ def convert_to_float( None If error occurred and cannot convert the value. """ - if isinstance(value, float): - return round(value, 1) - elif value is None or value == "None": + if value is None or value == "None": return None - else: - try: - return round(float(str(format(float(value), ".1f"))), 1) - except (ValueError, TypeError, AttributeError, KeyError): - _LOGGER.debug( - f"better thermostat {instance_name}: Could not convert '{value}' to float in {context}" - ) - return None - - -def calibration_round(value: Union[int, float, None]) -> Union[float, int, None]: - """Round the calibration value to the nearest 0.5. - - Parameters - ---------- - value : float - the value to round - - Returns - ------- - float - the rounded value - """ - if value is None: + try: + return round_by_steps(float(value), 10) + except (ValueError, TypeError, AttributeError, KeyError): + _LOGGER.debug( + f"better thermostat {instance_name}: Could not convert '{value}' to float in {context}" + ) return None - split = str(float(str(value))).split(".", 1) - decimale = int(split[1]) - if decimale > 8: - return float(str(split[0])) + 1.0 - else: - return float(str(split[0])) -def round_by_steps( - value: Union[int, float, None], steps: Union[int, float, None] -) -> Union[float, int, None]: - """Round the value based on the allowed decimal 'steps'. - Parameters - ---------- - value : float - the value to round +class rounding(Enum): + # rounding functions that avoid errors due to using floats - Returns - ------- - float - the rounded value - """ - if value is None: - return None - split = str(float(str(steps))).split(".", 1) - decimals = len(split[1]) + def up(x: float) -> float: + return math.ceil(x - 0.0001) - value_f = float(str(value)) - steps_f = float(str(steps)) - value_mod = value_f - (value_f % steps_f) + def down(x: float) -> float: + return math.floor(x + 0.0001) - return round(value_mod, decimals) + def nearest(x: float) -> float: + return round(x - 0.0001) -def round_down_to_half_degree( - value: Union[int, float, None] -) -> Union[float, int, None]: - """Round the value down to the nearest 0.5. +def round_by_steps( + value: Union[float, None], steps: Union[float, None], f_rounding: rounding = rounding.nearest +) -> Union[float, None]: + """Round the value based on the allowed decimal 'steps'. Parameters ---------- @@ -176,63 +146,10 @@ def round_down_to_half_degree( float the rounded value """ - if value is None: - return None - split = str(float(str(value))).split(".", 1) - decimale = int(split[1]) - if decimale >= 5: - if float(split[0]) > 0: - return float(str(split[0])) + 0.5 - else: - return float(str(split[0])) - 0.5 - else: - return float(str(split[0])) - - -def round_to_half_degree(value: Union[int, float, None]) -> Union[float, int, None]: - """Rounds numbers to the nearest n.5/n.0 - - Parameters - ---------- - value : int, float - input value - - Returns - ------- - float, int - either an int, if input was an int, or a float rounded to n.5/n.0 - - """ - if value is None: - return None - elif isinstance(value, float): - return round(value * 2) / 2 - elif isinstance(value, int): - return value - - -def round_to_hundredth_degree( - value: Union[int, float, None] -) -> Union[float, int, None]: - """Rounds numbers to the nearest n.nn0 - Parameters - ---------- - value : int, float - input value - - Returns - ------- - float, int - either an int, if input was an int, or a float rounded to n.nn0 - - """ - if value is None: + if value is None or steps is None: return None - elif isinstance(value, float): - return round(value * 100) / 100 - elif isinstance(value, int): - return value + return f_rounding(value * steps) / steps def check_float(potential_float): @@ -452,12 +369,13 @@ async def get_device_model(self, entity_id): entry = entity_reg.async_get(entity_id) dev_reg = dr.async_get(self.hass) device = dev_reg.async_get(entry.device_id) - _LOGGER.debug(f"better_thermostat {self.name}: found device:") + _LOGGER.debug(f"better_thermostat {self.device_name}: found device:") _LOGGER.debug(device) try: # Z2M reports the device name as a long string with the actual model name in braces, we need to extract it - return re.search("\\((.+?)\\)", device.model).group(1) - except AttributeError: + matches = re.findall(r"\((.+?)\)", device.model) + return matches[-1] + except IndexError: # Other climate integrations might report the model name plainly, need more infos on this return device.model except ( diff --git a/custom_components/better_thermostat/utils/watcher.py b/custom_components/better_thermostat/utils/watcher.py index 5c7a1969..cc75c47b 100644 --- a/custom_components/better_thermostat/utils/watcher.py +++ b/custom_components/better_thermostat/utils/watcher.py @@ -24,7 +24,7 @@ async def check_entity(self, entity) -> bool: "unavailable", ): _LOGGER.debug( - f"better_thermostat {self.name}: {entity} is unavailable. with state {state}" + f"better_thermostat {self.device_name}: {entity} is unavailable. with state {state}" ) return False if entity in self.devices_errors: @@ -66,7 +66,10 @@ async def check_all_entities(self) -> bool: learn_more_url="https://better-thermostat.org/qanda/missing_entity", severity=ir.IssueSeverity.WARNING, translation_key="missing_entity", - translation_placeholders={"entity": str(name), "name": str(self.name)}, + translation_placeholders={ + "entity": str(name), + "name": str(self.device_name), + }, ) return False return True diff --git a/custom_components/better_thermostat/utils/weather.py b/custom_components/better_thermostat/utils/weather.py index 62b9ecca..76992066 100644 --- a/custom_components/better_thermostat/utils/weather.py +++ b/custom_components/better_thermostat/utils/weather.py @@ -45,7 +45,7 @@ async def check_weather(self) -> bool: # TODO: add condition if heating period (oct-mar) then set it to true? _LOGGER.warning( "better_thermostat %s: no outdoor sensor data found. fallback to heat", - self.name, + self.device_name, ) _call_for_heat_outdoor = True else: @@ -74,12 +74,14 @@ async def check_weather_prediction(self) -> bool: if not successful """ if self.weather_entity is None: - _LOGGER.warning(f"better_thermostat {self.name}: weather entity not available.") + _LOGGER.warning( + f"better_thermostat {self.device_name}: weather entity not available." + ) return None if self.off_temperature is None or not isinstance(self.off_temperature, float): _LOGGER.warning( - f"better_thermostat {self.name}: off_temperature not set or not a float." + f"better_thermostat {self.device_name}: off_temperature not set or not a float." ) return None @@ -99,7 +101,7 @@ async def check_weather_prediction(self) -> bool: "temperature" ) ), - self.name, + self.device_name, "check_weather_prediction()", ) max_forecast_temp = int( @@ -107,12 +109,12 @@ async def check_weather_prediction(self) -> bool: ( convert_to_float( str(forecast[0]["temperature"]), - self.name, + self.device_name, "check_weather_prediction()", ) + convert_to_float( str(forecast[1]["temperature"]), - self.name, + self.device_name, "check_weather_prediction()", ) ) @@ -126,7 +128,9 @@ async def check_weather_prediction(self) -> bool: else: raise TypeError except TypeError: - _LOGGER.warning(f"better_thermostat {self.name}: no weather entity data found.") + _LOGGER.warning( + f"better_thermostat {self.device_name}: no weather entity data found." + ) return None @@ -145,13 +149,13 @@ async def check_ambient_air_temperature(self): if self.off_temperature is None or not isinstance(self.off_temperature, float): _LOGGER.warning( - f"better_thermostat {self.name}: off_temperature not set or not a float." + f"better_thermostat {self.device_name}: off_temperature not set or not a float." ) return None self.last_avg_outdoor_temp = convert_to_float( self.hass.states.get(self.outdoor_sensor).state, - self.name, + self.device_name, "check_ambient_air_temperature()", ) if "recorder" in self.hass.config.components: @@ -183,7 +187,9 @@ async def check_ambient_air_temperature(self): if item.state not in ("unknown", "unavailable"): _temp_history.add_measurement( convert_to_float( - item.state, self.name, "check_ambient_air_temperature()" + item.state, + self.device_name, + "check_ambient_air_temperature()", ), datetime.fromtimestamp(item.last_updated.timestamp()), ) @@ -195,7 +201,7 @@ async def check_ambient_air_temperature(self): avg_temp = self.last_avg_outdoor_temp _LOGGER.debug( - f"better_thermostat {self.name}: avg outdoor temp: {avg_temp}, threshold is {self.off_temperature}" + f"better_thermostat {self.device_name}: avg outdoor temp: {avg_temp}, threshold is {self.off_temperature}" ) if avg_temp is not None: diff --git a/docs/Configuration/configuration.md b/docs/Configuration/configuration.md index f841cdf4..bf9d80d7 100644 --- a/docs/Configuration/configuration.md +++ b/docs/Configuration/configuration.md @@ -10,7 +10,7 @@ permalink: configuration ** Goto: `Settings` -> `Devices & Services` -> `Integrations` -> `+ Add Integration` -> `Better Thermostat` ** -or just click on the button below: +or click on the button below: Open your Home Assistant instance and start setting up a new integration. @@ -25,11 +25,11 @@ or just click on the button below: **The real thermostat** This is a required field. This is the real climate entity you want to control with BT, if you have more than one climate in your room you can select multiple climate entities, fill out the first field and a second one will appear. -**Temperature sensor** This is a required field. This is the temperature sensor you want to use to control the real climate entity. It's used to get a more accurate temperature reading than the sensor in the real climate entity. Because you can place it in the middle of the room and not close to the radiator. +**Temperature sensor** This is a required field. This is the temperature sensor you want to use to control the real climate entity. It's used to get a more accurate temperature reading than the sensor in the real climate entity because you can place it in the middle of the room and not close to the radiator. **Humidity sensor** This is an optional field. For now, the humidity is only used to display it in the UI. In the future, it will be used to make a better calculation of the temperature or set it up to a *feels-like* temperature. -**If you have an outdoor sensor...** This is an optional field. If you have an outdoor sensor you can use it to get the outdoor temperatures, this is used to set the thermostat on or off if the threshold (the last option in this screen) is reached. It uses a mean of the last 3 days and checks it every morning at 5:00 AM. +**If you have an outdoor sensor...** This field is optional. If you have an outdoor sensor you can use it to get the outdoor temperatures, this is used to set the thermostat on or off if the threshold (the last option in this screen) is reached. It uses a mean of the last 3 days and checks it every morning at 5:00 AM. **Window Sensor** This is an optional field. If you have a window sensor you can use it to turn off the thermostat if the window is open and turn it on again when the window is closed. If you have more than one window in a room, you can also select window groups (see the GitHub page for more info). @@ -53,7 +53,7 @@ group: **The outdoor temperature threshold** This is an optional field. If you have an outdoor sensor or a weather entity, you can set a threshold. If the outdoor temperature is higher than the threshold, the thermostat will be turned off. If the outdoor temperature is lower than the threshold, the thermostat will be turned on. If you don't have an outdoor sensor or a weather entity, this field will be ignored. -**Tolerance** This is an optional field. It helps prevent the thermostat from turning on an off too often. Here is an example how it works: If you set the target temperature to 20.0 and the tolerance to 0.3 for example. Then BT will heat to 20.0 and then go to idle until the temperature drops again to 19.7 and then it will heat again to 20.0. +**Tolerance** This is an optional field. It helps prevent the thermostat from turning on and off too often. Here is an example of how it works: If you set the target temperature to 20.0 and the tolerance to 0.3 for example. Then BT will heat to 20.0 and then go to idle until the temperature drops again to 19.7 and then it will heat again to 20.0. ## Second step @@ -82,6 +82,6 @@ group: **If auto means heat for your TRV and you want to swap it** Some climates in HA use the mode auto for default heating, and a boost when mode is heat. This isn't what we want, so if this is the case for you, check this option. -**Ignore all inputs on the TRV like a child lock** If this option is enabled, all changes on the real TRV, even over HA, will be ignored or reverted, only input from the BT entity are accepted. +**Ignore all inputs on the TRV like a child lock** If this option is enabled, all changes on the real TRV, even over HA, will be ignored or reverted, only input from the BT entity is accepted. **If you use HomematicIP you should enable this...** If your entity is a HomematicIP entity this option should be enabled, to prevent a duty cycle overload. diff --git a/docs/Q&A/debuggin.md b/docs/Q&A/debugging.md similarity index 94% rename from docs/Q&A/debuggin.md rename to docs/Q&A/debugging.md index 5b69b850..5ec03474 100644 --- a/docs/Q&A/debuggin.md +++ b/docs/Q&A/debugging.md @@ -8,7 +8,7 @@ parent: Q&A --- # How do I activate the debug mode? -Basically, there are two options to enable debug mode. +There are two options to enable debug mode. ## Via configuration.yaml Add the following lines to your configuration.yaml file and restart Home Assistant. diff --git a/docs/Q&A/modes.md b/docs/Q&A/modes.md index 564d1ab4..c6b2e594 100644 --- a/docs/Q&A/modes.md +++ b/docs/Q&A/modes.md @@ -10,4 +10,4 @@ parent: Q&A # What is the difference between "local calibration" and "temperature-based calibration"? Local calibration means that the temperature reported by the TRV will be corrected to the temperature of the external thermometer with an offset. The benefit of this calibration is that the temperature shown on the TRV corresponds to the temperature measured via your selected external thermometer and the desired temperature you have set via BT and not the temperature measured via the TRV. -Target temperature based method: in this case, the difference between the measured temperatures at the TRV and the external thermometer will be adjusted via the target temperature set via BT. This makes it possible to use an external thermometer even if your TRV does not have the option to use an offset. The drawback is that your TRV will keep showing its own temperatures and not the ones set via BT. +Target temperature-based method: in this case, the difference between the measured temperatures at the TRV and the external thermometer will be adjusted via the target temperature set via BT. This makes it possible to use an external thermometer even if your TRV does not have the option to use an offset. The drawback is that your TRV will keep showing its own temperatures and not the ones set via BT. diff --git a/docs/Q&A/supported.md b/docs/Q&A/supported.md index c7d015cb..6178ee26 100644 --- a/docs/Q&A/supported.md +++ b/docs/Q&A/supported.md @@ -18,4 +18,4 @@ Currently, these are the integrations compatible with local calibration mode: - Deconz Please keep in mind that even if BT supports your integration, if your device does not support "local_temperature_calibration" this feature will not be available to you. You can check your device compatibility via Zigbee2MQTT. -If your preferred integration isn’t currently available for local calibration please open a GitHub issue. +If your preferred integration isn’t currently available for local calibration please open a [GitHub issue](https://github.com/KartoffelToby/better_thermostat/issues). diff --git a/docs/schedule.md b/docs/schedule.md index 3b18b59a..50d359e5 100644 --- a/docs/schedule.md +++ b/docs/schedule.md @@ -18,8 +18,8 @@ Services you can call from Home Assistant to set a temporary target temperature # How can I set up a night mode schedule? -Basically you can set up an automation that triggers a service call for every climate entity. -As an example you can use this blueprint: +You can set up an automation that triggers a service call for every climate entity. +As an example, you can use this blueprint: Open your Home Assistant instance and show the blueprint import dialog with a specific blueprint pre-filled.