From afa30c7e0acfa91ad68390eb89120e9f50e35b92 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Tue, 16 Feb 2021 20:44:19 +0100 Subject: [PATCH] New option for climate device to expose temperture sensors (#595) * Expose temperature sensors for climate * Fix config name * Cleanup * Fixes + cleanup * Added test * isort fix + changelog * Rename "expose_sensor" for weather & climate to "create_sensor" * Changelog adjustment --- changelog.md | 8 +++-- docs/weather.md | 4 +-- .../custom_components/xknx/factory.py | 5 +++- .../custom_components/xknx/schema.py | 8 +++-- test/config_tests/config_v1_test.py | 4 +-- .../resources/weather/invalid_1.yaml | 2 +- .../resources/weather/invalid_2.yaml | 2 +- .../resources/weather/valid_1.yaml | 2 +- .../resources/weather/valid_2.yaml | 2 +- .../resources/xknx/invalid_1.yaml | 2 +- .../resources/xknx/invalid_2.yaml | 2 +- .../resources/xknx/invalid_3.yaml | 2 +- test/config_tests/resources/xknx/valid_1.yaml | 2 +- test/config_tests/resources/xknx/valid_2.yaml | 2 +- test/config_tests/resources/xknx/valid_3.yaml | 2 +- test/devices_tests/climate_test.py | 25 ++++++++++++++++ test/devices_tests/weather_test.py | 6 ++-- test/str_test.py | 2 +- xknx.yaml | 4 +-- xknx/config/schema.py | 6 ++-- xknx/devices/climate.py | 29 ++++++++++++++++++- xknx/devices/weather.py | 12 ++++---- .../remote_value/remote_value_climate_mode.py | 6 ++-- xknx_v2.yaml | 2 +- 24 files changed, 102 insertions(+), 39 deletions(-) diff --git a/changelog.md b/changelog.md index 7911553b7..8692b1d26 100644 --- a/changelog.md +++ b/changelog.md @@ -5,6 +5,8 @@ ### Devices - BinarySensor: return `None` for `BinarySensor.counter` when context timeout is not used (and don't calculate it) +- Climate: Add `create_temperature_sensors` option to create dedicated sensors for current and target temperature. +- Weather (breaking change!): Renamed `expose_sensors` to `create_sensors` to prevent confusion with the XKNX `expose_sensor` device type. ### Internals @@ -21,11 +23,11 @@ - Fan: Add `max_step` attribute which defines the maximum amount of steps. If set, the fan is controlled by steps instead of percentage. - Fan: Add `group_address_oscillation` and `group_address_oscillation_state` attributes to control the oscillation of a fan. -## 0.16.2 Bugfix for yaml loader 2021-01-24 +## 0.16.2 Bugfix for YAML loader 2021-01-24 ### Internals -- fix conflict with HA Yaml loader +- fix conflict with HA YAML loader ## 0.16.1 HA register services 2021-01-16 @@ -177,7 +179,7 @@ - Reset binary sensor counters after the context has been timed out in order to be able to use state change events within HA - Code cleanups -## 0.14.0 New sensor types and refacoring of binary sensor automations +## 0.14.0 New sensor types and refactoring of binary sensor automations ### Breaking changes diff --git a/docs/weather.md b/docs/weather.md index 11eec415d..c28fd837a 100644 --- a/docs/weather.md +++ b/docs/weather.md @@ -51,7 +51,7 @@ groups: group_address_day_night: "7/0/8", group_address_air_pressure: "7/0/9", group_address_humidity: "7/0/10", - expose_sensors: True, + create_sensors: True, sync_state: True, } ``` @@ -71,7 +71,7 @@ groups: - **group_address_day_night** KNX address for reading a day/night object. - **group_address_air_pressure** KNX address reading current air pressure. **DPT 9.006** - **group_address_humidity** KNX address for reading current humidity. **DPT 9.007** -- **expose_sensors** If true, also exposes all values as sensors to the xknx device list (useful for home assistant). Default: False +- **create_sensors** If true, also adds sensors for all values to the xknx device list (useful for Home Assistant). Default: False - **sync_state** Periodically sync the state. - **device_updated_cb** awaitable callback for each update. diff --git a/home-assistant-plugin/custom_components/xknx/factory.py b/home-assistant-plugin/custom_components/xknx/factory.py index cee6b67a8..3f3080e93 100644 --- a/home-assistant-plugin/custom_components/xknx/factory.py +++ b/home-assistant-plugin/custom_components/xknx/factory.py @@ -259,6 +259,9 @@ def _create_climate(knx_module: XKNX, config: ConfigType) -> XknxClimate: max_temp=config.get(ClimateSchema.CONF_MAX_TEMP), mode=climate_mode, on_off_invert=config[ClimateSchema.CONF_ON_OFF_INVERT], + create_temperature_sensors=config.get( + ClimateSchema.CONF_CREATE_TEMPERATURE_SENSORS + ), ) @@ -327,7 +330,7 @@ def _create_weather(knx_module: XKNX, config: ConfigType) -> XknxWeather: knx_module, name=config[CONF_NAME], sync_state=config[WeatherSchema.CONF_SYNC_STATE], - expose_sensors=config[WeatherSchema.CONF_XKNX_EXPOSE_SENSORS], + create_sensors=config[WeatherSchema.CONF_XKNX_CREATE_SENSORS], group_address_temperature=config[WeatherSchema.CONF_XKNX_TEMPERATURE_ADDRESS], group_address_brightness_south=config.get( WeatherSchema.CONF_XKNX_BRIGHTNESS_SOUTH_ADDRESS diff --git a/home-assistant-plugin/custom_components/xknx/schema.py b/home-assistant-plugin/custom_components/xknx/schema.py index dcf321c32..bca4445c2 100644 --- a/home-assistant-plugin/custom_components/xknx/schema.py +++ b/home-assistant-plugin/custom_components/xknx/schema.py @@ -240,6 +240,7 @@ class ClimateSchema: CONF_ON_OFF_INVERT = "on_off_invert" CONF_MIN_TEMP = "min_temp" CONF_MAX_TEMP = "max_temp" + CONF_CREATE_TEMPERATURE_SENSORS = "create_temperature_sensors" DEFAULT_NAME = "KNX Climate" DEFAULT_SETPOINT_SHIFT_MODE = "DPT6010" @@ -295,6 +296,9 @@ class ClimateSchema: ), vol.Optional(CONF_MIN_TEMP): vol.Coerce(float), vol.Optional(CONF_MAX_TEMP): vol.Coerce(float), + vol.Optional( + CONF_CREATE_TEMPERATURE_SENSORS, default=False + ): cv.boolean, } ), ) @@ -403,7 +407,7 @@ class WeatherSchema: CONF_XKNX_DAY_NIGHT_ADDRESS = "address_day_night" CONF_XKNX_AIR_PRESSURE_ADDRESS = "address_air_pressure" CONF_XKNX_HUMIDITY_ADDRESS = "address_humidity" - CONF_XKNX_EXPOSE_SENSORS = "expose_sensors" + CONF_XKNX_CREATE_SENSORS = "create_sensors" DEFAULT_NAME = "KNX Weather Station" @@ -415,7 +419,7 @@ class WeatherSchema: cv.boolean, cv.string, ), - vol.Optional(CONF_XKNX_EXPOSE_SENSORS, default=False): cv.boolean, + vol.Optional(CONF_XKNX_CREATE_SENSORS, default=False): cv.boolean, vol.Required(CONF_XKNX_TEMPERATURE_ADDRESS): cv.string, vol.Optional(CONF_XKNX_BRIGHTNESS_SOUTH_ADDRESS): cv.string, vol.Optional(CONF_XKNX_BRIGHTNESS_EAST_ADDRESS): cv.string, diff --git a/test/config_tests/config_v1_test.py b/test/config_tests/config_v1_test.py index a850da195..d0d224fea 100644 --- a/test/config_tests/config_v1_test.py +++ b/test/config_tests/config_v1_test.py @@ -587,12 +587,12 @@ def test_config_weather(self): group_address_day_night="7/0/8", group_address_air_pressure="7/0/9", group_address_humidity="7/0/10", - expose_sensors=False, + create_sensors=False, sync_state=True, ), ) - def test_config_weather_expose_sensor(self): + def test_config_weather_create_sensor(self): """Test reading weather from config file.""" self.assertTrue(isinstance(TestConfig.xknx.devices["Home_temperature"], Sensor)) self.assertTrue( diff --git a/test/config_tests/resources/weather/invalid_1.yaml b/test/config_tests/resources/weather/invalid_1.yaml index 4e990aeab..7e535b810 100644 --- a/test/config_tests/resources/weather/invalid_1.yaml +++ b/test/config_tests/resources/weather/invalid_1.yaml @@ -33,4 +33,4 @@ air_pressure: humidity: state_address: "7/0/10" state_update: "expire 60" -expose_sensors: True \ No newline at end of file +create_sensors: True \ No newline at end of file diff --git a/test/config_tests/resources/weather/invalid_2.yaml b/test/config_tests/resources/weather/invalid_2.yaml index cd1969aa5..a6f6980e3 100644 --- a/test/config_tests/resources/weather/invalid_2.yaml +++ b/test/config_tests/resources/weather/invalid_2.yaml @@ -34,4 +34,4 @@ air_pressure: humidity: state_address: "7/0/10" state_update: "expire 60" -expose_sensors: True \ No newline at end of file +create_sensors: True \ No newline at end of file diff --git a/test/config_tests/resources/weather/valid_1.yaml b/test/config_tests/resources/weather/valid_1.yaml index a59ee24bb..c0fb1d299 100644 --- a/test/config_tests/resources/weather/valid_1.yaml +++ b/test/config_tests/resources/weather/valid_1.yaml @@ -36,4 +36,4 @@ air_pressure: humidity: state_address: "7/0/10" state_update: "expire 60" -expose_sensors: True \ No newline at end of file +create_sensors: True \ No newline at end of file diff --git a/test/config_tests/resources/weather/valid_2.yaml b/test/config_tests/resources/weather/valid_2.yaml index 85c2cd8d8..5d9f98968 100644 --- a/test/config_tests/resources/weather/valid_2.yaml +++ b/test/config_tests/resources/weather/valid_2.yaml @@ -3,4 +3,4 @@ friendly_name: "Home" temperature: state_address: "7/0/0" state_update: "expire 60" -expose_sensors: "off" \ No newline at end of file +create_sensors: "off" \ No newline at end of file diff --git a/test/config_tests/resources/xknx/invalid_1.yaml b/test/config_tests/resources/xknx/invalid_1.yaml index 1ae4ad5c5..261e172f2 100644 --- a/test/config_tests/resources/xknx/invalid_1.yaml +++ b/test/config_tests/resources/xknx/invalid_1.yaml @@ -157,7 +157,7 @@ weather: humidity: state_address: "7/0/10" state_update: "expire 60" - expose_sensors: True + create_sensors: True datetime: - name: "Generaltime" time: diff --git a/test/config_tests/resources/xknx/invalid_2.yaml b/test/config_tests/resources/xknx/invalid_2.yaml index f02bf8abd..bc465c53a 100644 --- a/test/config_tests/resources/xknx/invalid_2.yaml +++ b/test/config_tests/resources/xknx/invalid_2.yaml @@ -155,7 +155,7 @@ weather: humidity: state_address: "7/0/10" state_update: "expire 60" - expose_sensors: True + create_sensors: True datetime: - name: "Generaltime" time: diff --git a/test/config_tests/resources/xknx/invalid_3.yaml b/test/config_tests/resources/xknx/invalid_3.yaml index 9a1be4b3e..1c70d10da 100644 --- a/test/config_tests/resources/xknx/invalid_3.yaml +++ b/test/config_tests/resources/xknx/invalid_3.yaml @@ -154,7 +154,7 @@ weather: humidity: state_address: "7/0/10" state_update: "expire 60" - expose_sensors: True + create_sensors: True datetime: - name: "Generaltime" time: diff --git a/test/config_tests/resources/xknx/valid_1.yaml b/test/config_tests/resources/xknx/valid_1.yaml index 7fb814559..e9948bd04 100644 --- a/test/config_tests/resources/xknx/valid_1.yaml +++ b/test/config_tests/resources/xknx/valid_1.yaml @@ -160,7 +160,7 @@ weather: humidity: state_address: "7/0/10" state_update: "expire 60" - expose_sensors: True + create_sensors: True datetime: - name: "Generaltime" time: diff --git a/test/config_tests/resources/xknx/valid_2.yaml b/test/config_tests/resources/xknx/valid_2.yaml index 80ea1cb56..233bbc3e8 100644 --- a/test/config_tests/resources/xknx/valid_2.yaml +++ b/test/config_tests/resources/xknx/valid_2.yaml @@ -157,7 +157,7 @@ weather: humidity: state_address: "7/0/10" state_update: "expire 60" - expose_sensors: True + create_sensors: True datetime: - name: "Generaltime" time: diff --git a/test/config_tests/resources/xknx/valid_3.yaml b/test/config_tests/resources/xknx/valid_3.yaml index 505e163f0..67717c12d 100644 --- a/test/config_tests/resources/xknx/valid_3.yaml +++ b/test/config_tests/resources/xknx/valid_3.yaml @@ -162,7 +162,7 @@ weather: humidity: state_address: "7/0/10" state_update: "expire 60" - expose_sensors: True + create_sensors: True datetime: - name: "Generaltime" time: diff --git a/test/devices_tests/climate_test.py b/test/devices_tests/climate_test.py index 69c75e0a8..ab8f123e4 100644 --- a/test/devices_tests/climate_test.py +++ b/test/devices_tests/climate_test.py @@ -1456,3 +1456,28 @@ def test_power_on_off(self): payload=GroupValueWrite(DPTBinary(True)), ), ) + + # + # Create temperature sensor tests + # + def test_create_sensor(self): + """Test default state mapping.""" + xknx = XKNX() + Climate( + name="climate", + xknx=xknx, + group_address_temperature="5/1/1", + group_address_target_temperature="5/1/4", + ) + + self.assertEqual(len(xknx.devices), 1) + + Climate( + name="climate", + xknx=xknx, + group_address_temperature="5/1/1", + group_address_target_temperature="5/1/4", + create_temperature_sensors=True, + ) + + self.assertEqual(len(xknx.devices), 3) diff --git a/test/devices_tests/weather_test.py b/test/devices_tests/weather_test.py index 942aaeda6..5df3806ff 100644 --- a/test/devices_tests/weather_test.py +++ b/test/devices_tests/weather_test.py @@ -291,9 +291,9 @@ def test_weather_default(self): self.assertEqual(weather.ha_current_state(), WeatherCondition.exceptional) # - # Expose Sensor tests + # Create sensor tests # - def test_expose_sensor(self): + def test_create_sensor(self): """Test default state mapping.""" xknx = XKNX() Weather( @@ -313,7 +313,7 @@ def test_expose_sensor(self): group_address_brightness_south="1/3/6", group_address_brightness_west="1/3/7", group_address_wind_alarm="1/5/4", - expose_sensors=True, + create_sensors=True, ) self.assertEqual(len(xknx.devices), 6) diff --git a/test/str_test.py b/test/str_test.py index 0833f5e24..b8b3233eb 100644 --- a/test/str_test.py +++ b/test/str_test.py @@ -339,7 +339,7 @@ def test_weather(self): group_address_day_night="7/0/7", group_address_rain_alarm="7/0/0", group_address_frost_alarm="7/0/8", - expose_sensors=True, + create_sensors=True, group_address_air_pressure="7/0/9", group_address_humidity="7/0/9", group_address_wind_alarm="7/0/10", diff --git a/xknx.yaml b/xknx.yaml index a960c0465..f8be7300c 100644 --- a/xknx.yaml +++ b/xknx.yaml @@ -288,7 +288,7 @@ groups: group_address_day_night: "7/0/8", group_address_air_pressure: "7/0/9", group_address_humidity: "7/0/10", - expose_sensors: True, + create_sensors: True, sync_state: True, } Remote: @@ -304,6 +304,6 @@ groups: group_address_day_night: "7/0/8", group_address_air_pressure: "7/0/9", group_address_humidity: "7/0/10", - expose_sensors: False, + create_sensors: False, sync_state: True, } diff --git a/xknx/config/schema.py b/xknx/config/schema.py index c49fd032d..58fc6bd01 100644 --- a/xknx/config/schema.py +++ b/xknx/config/schema.py @@ -323,6 +323,7 @@ class ClimateSchema: CONF_HEAT_COOL = "heat_cool" CONF_OPERATION_MODES = "operation_modes" CONF_ON_OFF = "on_off" + CONF_CREATE_TEMPERATURE_SENSORS = "create_temperature_sensors" CONF_FROST_PROTECTION_ADDRESS = "frost_protection_address" CONF_NIGHT_ADDRESS = "night_address" @@ -416,6 +417,7 @@ class ClimateSchema: vol.Optional(CONF_OPERATION_MODES): vol.All( ensure_list, [vol.In({**OPERATION_MODES, **PRESET_MODES})] ), + vol.Optional(CONF_CREATE_TEMPERATURE_SENSORS, default=False): boolean, } ) @@ -435,7 +437,7 @@ class WeatherSchema: CONF_DAY_NIGHT = "day_night" CONF_AIR_PRESSURE = "air_pressure" CONF_HUMIDITY = "humidity" - CONF_EXPOSE_SENSORS = "expose_sensors" + CONF_CREATE_SENSORS = "create_sensors" SCHEMA = BaseDeviceSchema.SCHEMA.extend( { @@ -511,7 +513,7 @@ class WeatherSchema: vol.Required(CONF_STATE_ADDRESS): ensure_group_address, } ), - vol.Optional(CONF_EXPOSE_SENSORS, default=False): boolean, + vol.Optional(CONF_CREATE_SENSORS, default=False): boolean, } ) diff --git a/xknx/devices/climate.py b/xknx/devices/climate.py index eb47698e4..5bb5af2a1 100644 --- a/xknx/devices/climate.py +++ b/xknx/devices/climate.py @@ -16,6 +16,7 @@ from .climate_mode import ClimateMode from .device import Device, DeviceCallbackType +from .sensor import Sensor if TYPE_CHECKING: from xknx.remote_value import RemoteValue @@ -62,6 +63,7 @@ def __init__( min_temp: Optional[float] = None, max_temp: Optional[float] = None, mode: Optional[ClimateMode] = None, + create_temperature_sensors: bool = False, device_updated_cb: Optional[DeviceCallbackType] = None, ): """Initialize Climate class.""" @@ -78,7 +80,7 @@ def __init__( xknx, group_address_state=group_address_temperature, device_name=self.name, - feature_name="Current Temperature", + feature_name="Current temperature", after_update_cb=self.after_update, ) @@ -125,6 +127,9 @@ def __init__( self.mode = mode + if create_temperature_sensors: + self.create_temperature_sensors() + def _iter_remote_values(self) -> Iterator["RemoteValue[Any]"]: """Iterate the devices RemoteValue classes.""" yield from ( @@ -134,6 +139,28 @@ def _iter_remote_values(self) -> Iterator["RemoteValue[Any]"]: self.on, ) + def create_temperature_sensors(self) -> None: + """Create temperature sensors.""" + for suffix, group_address, value_type in ( + ( + "temperature", + self.temperature.group_address_state, + "temperature", + ), + ( + "target temperature", + self.target_temperature.group_address_state, + "temperature", + ), + ): + if group_address is not None: + Sensor( + self.xknx, + name=self.name + " " + suffix, + group_address_state=group_address, + value_type=value_type, + ) + @classmethod def from_config(cls, xknx: "XKNX", name: str, config: Any) -> "Climate": """Initialize object from configuration structure.""" diff --git a/xknx/devices/weather.py b/xknx/devices/weather.py index e34972bc1..94e4f3a39 100644 --- a/xknx/devices/weather.py +++ b/xknx/devices/weather.py @@ -92,7 +92,7 @@ def __init__( group_address_day_night: Optional["GroupAddressableType"] = None, group_address_air_pressure: Optional["GroupAddressableType"] = None, group_address_humidity: Optional["GroupAddressableType"] = None, - expose_sensors: bool = False, + create_sensors: bool = False, sync_state: bool = True, device_updated_cb: Optional[DeviceCallbackType] = None, ) -> None: @@ -212,8 +212,8 @@ def __init__( after_update_cb=self.after_update, ) - if expose_sensors: - self.expose_sensors() + if create_sensors: + self.create_sensors() def _iter_remote_values(self) -> Iterator[RemoteValue[Any]]: """Iterate the devices remote values.""" @@ -317,7 +317,7 @@ def max_brightness(self) -> float: self.brightness_east, ) - def expose_sensors(self) -> None: + def create_sensors(self) -> None: """Expose sensors to xknx.""" for suffix, group_address in ( ("_rain_alarm", self._rain_alarm.group_address_state), @@ -435,7 +435,7 @@ def from_config(cls, xknx: "XKNX", name: str, config: Any) -> "Weather": group_address_day_night = config.get("group_address_day_night") group_address_air_pressure = config.get("group_address_air_pressure") group_address_humidity = config.get("group_address_humidity") - expose_sensors = config.get("expose_sensors", False) + create_sensors = config.get("create_sensors", False) sync_state = config.get("sync_state", True) return cls( @@ -453,7 +453,7 @@ def from_config(cls, xknx: "XKNX", name: str, config: Any) -> "Weather": group_address_day_night=group_address_day_night, group_address_air_pressure=group_address_air_pressure, group_address_humidity=group_address_humidity, - expose_sensors=expose_sensors, + create_sensors=create_sensors, sync_state=sync_state, ) diff --git a/xknx/remote_value/remote_value_climate_mode.py b/xknx/remote_value/remote_value_climate_mode.py index a8a1db146..6756e9728 100644 --- a/xknx/remote_value/remote_value_climate_mode.py +++ b/xknx/remote_value/remote_value_climate_mode.py @@ -53,7 +53,7 @@ def __init__( group_address_state: Optional["GroupAddressableType"] = None, sync_state: bool = True, device_name: Optional[str] = None, - feature_name: str = "Climate Mode", + feature_name: str = "Climate mode", climate_mode_type: Optional[ClimateModeType] = None, after_update_cb: Optional[AsyncCallbackType] = None, passive_group_addresses: Optional[List["GroupAddressableType"]] = None, @@ -174,7 +174,7 @@ def __init__( group_address_state: Optional["GroupAddressableType"] = None, sync_state: bool = True, device_name: Optional[str] = None, - feature_name: str = "Climate Mode Binary", + feature_name: str = "Climate mode binary", after_update_cb: Optional[AsyncCallbackType] = None, operation_mode: Optional[HVACOperationMode] = None, ): @@ -260,7 +260,7 @@ def __init__( group_address_state: Optional["GroupAddressableType"] = None, sync_state: bool = True, device_name: Optional[str] = None, - feature_name: str = "Controller Mode Heat/Cool", + feature_name: str = "Controller mode Heat/Cool", after_update_cb: Optional[AsyncCallbackType] = None, controller_mode: Optional[HVACControllerMode] = None, ): diff --git a/xknx_v2.yaml b/xknx_v2.yaml index c2c5fd2d5..f564d212e 100644 --- a/xknx_v2.yaml +++ b/xknx_v2.yaml @@ -163,7 +163,7 @@ climate: humidity: state_address: "7/0/10" state_update: "expire 60" - expose_sensors: True + create_sensors: True datetime: - name: "Generaltime" time: