From d79ed3acbbc2f24f7e3644682f27aaf8218b9376 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 11 Oct 2022 10:49:54 +0200 Subject: [PATCH 01/13] Fix state saving when sharing topics for MQTT entities (#79421) * Do not write old state sharing availability topic * Add a test * Support for all availability topics * delay async_write_ha_state till last callback * Process write req after processing callback jobs * Do not count subscription callbacks * Simplify * Stale docsting * No topic needed for delays state write * No need to clear when reloading * Move test to test_mixins.py * Only set up sensor platform for test --- .../components/mqtt/alarm_control_panel.py | 4 +- .../components/mqtt/binary_sensor.py | 3 +- homeassistant/components/mqtt/client.py | 1 + homeassistant/components/mqtt/climate.py | 14 ++-- homeassistant/components/mqtt/cover.py | 6 +- .../mqtt/device_tracker/schema_discovery.py | 3 +- homeassistant/components/mqtt/fan.py | 12 +-- homeassistant/components/mqtt/humidifier.py | 12 +-- .../components/mqtt/light/schema_basic.py | 22 +++--- .../components/mqtt/light/schema_json.py | 4 +- .../components/mqtt/light/schema_template.py | 3 +- homeassistant/components/mqtt/lock.py | 3 +- homeassistant/components/mqtt/mixins.py | 6 +- homeassistant/components/mqtt/models.py | 21 ++++++ homeassistant/components/mqtt/number.py | 3 +- homeassistant/components/mqtt/select.py | 3 +- homeassistant/components/mqtt/sensor.py | 6 +- homeassistant/components/mqtt/siren.py | 3 +- homeassistant/components/mqtt/subscription.py | 7 +- homeassistant/components/mqtt/switch.py | 3 +- .../components/mqtt/vacuum/schema_legacy.py | 4 +- .../components/mqtt/vacuum/schema_state.py | 4 +- tests/components/mqtt/test_mixins.py | 73 +++++++++++++++++++ 23 files changed, 164 insertions(+), 56 deletions(-) create mode 100644 tests/components/mqtt/test_mixins.py diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index c3502cd8e64..ed1990d919e 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -49,7 +49,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttCommandTemplate, MqttValueTemplate -from .util import valid_publish_topic, valid_subscribe_topic +from .util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -211,7 +211,7 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): _LOGGER.warning("Received unexpected payload: %s", msg.payload) return self._state = payload - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) self._sub_state = subscription.async_prepare_subscribe_topics( self.hass, diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index f0e5ecc9df8..915a2780283 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -47,6 +47,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttValueTemplate +from .util import get_mqtt_data _LOGGER = logging.getLogger(__name__) @@ -260,7 +261,7 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity): self.hass, off_delay, off_delay_listener ) - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) self._sub_state = subscription.async_prepare_subscribe_topics( self.hass, diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index bd734318938..79d720338be 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -658,6 +658,7 @@ class MQTT: timestamp, ), ) + self._mqtt_data.state_write_requests.process_write_state_requests() def _mqtt_on_callback(self, _mqttc, _userdata, mid, _granted_qos=None) -> None: """Publish / Subscribe / Unsubscribe callback.""" diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 96c7ca3665b..9f98fcfebdc 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -55,7 +55,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttCommandTemplate, MqttValueTemplate -from .util import valid_publish_topic, valid_subscribe_topic +from .util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -494,7 +494,7 @@ class MqttClimate(MqttEntity, ClimateEntity): payload, ) return - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_subscription(topics, CONF_ACTION_TOPIC, handle_action_received) @@ -505,7 +505,7 @@ class MqttClimate(MqttEntity, ClimateEntity): try: setattr(self, attr, float(payload)) - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) except ValueError: _LOGGER.error("Could not parse temperature from %s", payload) @@ -564,7 +564,7 @@ class MqttClimate(MqttEntity, ClimateEntity): _LOGGER.error("Invalid %s mode: %s", mode_list, payload) else: setattr(self, attr, payload) - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) @callback @log_messages(self.hass, self.entity_id) @@ -623,7 +623,7 @@ class MqttClimate(MqttEntity, ClimateEntity): else: _LOGGER.error("Invalid %s mode: %s", attr, payload) - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) @callback @log_messages(self.hass, self.entity_id) @@ -640,7 +640,7 @@ class MqttClimate(MqttEntity, ClimateEntity): preset_mode = render_template(msg, CONF_PRESET_MODE_VALUE_TEMPLATE) if preset_mode in [PRESET_NONE, PAYLOAD_NONE]: self._preset_mode = None - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) return if not preset_mode: _LOGGER.debug("Ignoring empty preset_mode from '%s'", msg.topic) @@ -654,7 +654,7 @@ class MqttClimate(MqttEntity, ClimateEntity): ) else: self._preset_mode = preset_mode - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_subscription( topics, CONF_PRESET_MODE_STATE_TOPIC, handle_preset_mode_received diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 1f5d26c3a78..6ed12b8adef 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -51,7 +51,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttCommandTemplate, MqttValueTemplate -from .util import valid_publish_topic, valid_subscribe_topic +from .util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -405,7 +405,7 @@ class MqttCover(MqttEntity, CoverEntity): ) return - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) @callback @log_messages(self.hass, self.entity_id) @@ -451,7 +451,7 @@ class MqttCover(MqttEntity, CoverEntity): else STATE_OPEN ) - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._config.get(CONF_GET_POSITION_TOPIC): topics["get_position_topic"] = { diff --git a/homeassistant/components/mqtt/device_tracker/schema_discovery.py b/homeassistant/components/mqtt/device_tracker/schema_discovery.py index 907d424e8a4..1d3f9d109f6 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_discovery.py +++ b/homeassistant/components/mqtt/device_tracker/schema_discovery.py @@ -29,6 +29,7 @@ from ..const import CONF_QOS, CONF_STATE_TOPIC from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper from ..models import MqttValueTemplate +from ..util import get_mqtt_data CONF_PAYLOAD_HOME = "payload_home" CONF_PAYLOAD_NOT_HOME = "payload_not_home" @@ -106,7 +107,7 @@ class MqttDeviceTracker(MqttEntity, TrackerEntity): else: self._location_name = msg.payload - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) self._sub_state = subscription.async_prepare_subscribe_topics( self.hass, diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 584df08e7d7..866b429c68f 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -55,7 +55,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttCommandTemplate, MqttValueTemplate -from .util import valid_publish_topic, valid_subscribe_topic +from .util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic CONF_PERCENTAGE_STATE_TOPIC = "percentage_state_topic" CONF_PERCENTAGE_COMMAND_TOPIC = "percentage_command_topic" @@ -391,7 +391,7 @@ class MqttFan(MqttEntity, FanEntity): self._state = False elif payload == PAYLOAD_NONE: self._state = None - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_STATE_TOPIC] is not None: topics[CONF_STATE_TOPIC] = { @@ -413,7 +413,7 @@ class MqttFan(MqttEntity, FanEntity): return if rendered_percentage_payload == self._payload["PERCENTAGE_RESET"]: self._percentage = None - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) return try: percentage = ranged_value_to_percentage( @@ -436,7 +436,7 @@ class MqttFan(MqttEntity, FanEntity): ) return self._percentage = percentage - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_PERCENTAGE_STATE_TOPIC] is not None: topics[CONF_PERCENTAGE_STATE_TOPIC] = { @@ -469,7 +469,7 @@ class MqttFan(MqttEntity, FanEntity): return self._preset_mode = preset_mode - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_PRESET_MODE_STATE_TOPIC] is not None: topics[CONF_PRESET_MODE_STATE_TOPIC] = { @@ -492,7 +492,7 @@ class MqttFan(MqttEntity, FanEntity): self._oscillation = True elif payload == self._payload["OSCILLATE_OFF_PAYLOAD"]: self._oscillation = False - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_OSCILLATION_STATE_TOPIC] is not None: topics[CONF_OSCILLATION_STATE_TOPIC] = { diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index 837bbb8b909..7514f0ff672 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -51,7 +51,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttCommandTemplate, MqttValueTemplate -from .util import valid_publish_topic, valid_subscribe_topic +from .util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic CONF_AVAILABLE_MODES_LIST = "modes" CONF_DEVICE_CLASS = "device_class" @@ -309,7 +309,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): self._state = False elif payload == PAYLOAD_NONE: self._state = None - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_STATE_TOPIC] is not None: topics[CONF_STATE_TOPIC] = { @@ -331,7 +331,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): return if rendered_target_humidity_payload == self._payload["HUMIDITY_RESET"]: self._target_humidity = None - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) return try: target_humidity = round(float(rendered_target_humidity_payload)) @@ -355,7 +355,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): ) return self._target_humidity = target_humidity - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_TARGET_HUMIDITY_STATE_TOPIC] is not None: topics[CONF_TARGET_HUMIDITY_STATE_TOPIC] = { @@ -373,7 +373,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): mode = self._value_templates[ATTR_MODE](msg.payload) if mode == self._payload["MODE_RESET"]: self._mode = None - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) return if not mode: _LOGGER.debug("Ignoring empty mode from '%s'", msg.topic) @@ -388,7 +388,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): return self._mode = mode - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_MODE_STATE_TOPIC] is not None: topics[CONF_MODE_STATE_TOPIC] = { diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index e2805781f45..d435d4e91ad 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -51,7 +51,7 @@ from ..const import ( from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity from ..models import MqttCommandTemplate, MqttValueTemplate -from ..util import valid_publish_topic, valid_subscribe_topic +from ..util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic from .schema import MQTT_LIGHT_SCHEMA_SCHEMA _LOGGER = logging.getLogger(__name__) @@ -438,7 +438,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): self._state = False elif payload == PAYLOAD_NONE: self._state = None - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_STATE_TOPIC] is not None: topics[CONF_STATE_TOPIC] = { @@ -462,7 +462,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): device_value = float(payload) percent_bright = device_value / self._config[CONF_BRIGHTNESS_SCALE] self._brightness = percent_bright * 255 - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_BRIGHTNESS_STATE_TOPIC, brightness_received) @@ -493,7 +493,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): if not rgb: return self._rgb_color = rgb - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_RGB_STATE_TOPIC, rgb_received) @@ -510,7 +510,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): if not rgbw: return self._rgbw_color = rgbw - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_RGBW_STATE_TOPIC, rgbw_received) @@ -527,7 +527,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): if not rgbww: return self._rgbww_color = rgbww - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_RGBWW_STATE_TOPIC, rgbww_received) @@ -543,7 +543,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): return self._color_mode = payload - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_COLOR_MODE_STATE_TOPIC, color_mode_received) @@ -561,7 +561,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): if self._optimistic_color_mode: self._color_mode = ColorMode.COLOR_TEMP self._color_temp = int(payload) - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_COLOR_TEMP_STATE_TOPIC, color_temp_received) @@ -577,7 +577,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): return self._effect = payload - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_EFFECT_STATE_TOPIC, effect_received) @@ -594,7 +594,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): if self._optimistic_color_mode: self._color_mode = ColorMode.HS self._hs_color = hs_color - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) except ValueError: _LOGGER.debug("Failed to parse hs state update: '%s'", payload) @@ -613,7 +613,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): if self._optimistic_color_mode: self._color_mode = ColorMode.XY self._xy_color = xy_color - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_XY_STATE_TOPIC, xy_received) diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 8843e8542eb..a4a76673176 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -58,7 +58,7 @@ from ..const import ( ) from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity -from ..util import valid_subscribe_topic +from ..util import get_mqtt_data, valid_subscribe_topic from .schema import MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import ( CONF_BRIGHTNESS_SCALE, @@ -401,7 +401,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): with suppress(KeyError): self._effect = values["effect"] - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_STATE_TOPIC] is not None: self._sub_state = subscription.async_prepare_subscribe_topics( diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index dacc977a036..33c7f1cea1b 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -41,6 +41,7 @@ from ..const import ( from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity from ..models import MqttValueTemplate +from ..util import get_mqtt_data from .schema import MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import MQTT_LIGHT_ATTRIBUTES_BLOCKED @@ -256,7 +257,7 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): else: _LOGGER.warning("Unsupported effect value received") - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topics[CONF_STATE_TOPIC] is not None: self._sub_state = subscription.async_prepare_subscribe_topics( diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index dca02f909dc..c9bdd696896 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -33,6 +33,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttValueTemplate +from .util import get_mqtt_data CONF_PAYLOAD_LOCK = "payload_lock" CONF_PAYLOAD_UNLOCK = "payload_unlock" @@ -158,7 +159,7 @@ class MqttLock(MqttEntity, LockEntity): elif payload == self._config[CONF_STATE_UNLOCKED]: self._state = False - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._config.get(CONF_STATE_TOPIC) is None: # Force into optimistic mode. diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 8022a6e91ae..b5c870a196e 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -435,7 +435,9 @@ class MqttAttributes(Entity): and k not in self._attributes_extra_blocked } self._attributes = filtered_dict - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request( + self + ) else: _LOGGER.warning("JSON result was not a dictionary") self._attributes = None @@ -547,7 +549,7 @@ class MqttAvailability(Entity): self._available[topic] = False self._available_latest = False - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) self._available = { topic: (self._available[topic] if topic in self._available else False) diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index b7cb81b2ea4..f2f30419b4c 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -236,6 +236,26 @@ class MqttValueTemplate: ) +class EntityTopicState: + """Manage entity state write requests for subscribed topics.""" + + def __init__(self) -> None: + """Register topic.""" + self.subscribe_calls: dict[str, Entity] = {} + + @callback + def process_write_state_requests(self) -> None: + """Process the write state requests.""" + while self.subscribe_calls: + _, entity = self.subscribe_calls.popitem() + entity.async_write_ha_state() + + @callback + def write_state_request(self, entity: Entity) -> None: + """Register write state request.""" + self.subscribe_calls[entity.entity_id] = entity + + @dataclass class MqttData: """Keep the MQTT entry data.""" @@ -264,6 +284,7 @@ class MqttData: default_factory=dict ) reload_needed: bool = False + state_write_requests: EntityTopicState = field(default_factory=EntityTopicState) subscriptions_to_restore: list[Subscription] = field(default_factory=list) tags: dict[str, dict[str, MQTTTagScanner]] = field(default_factory=dict) updated_config: ConfigType = field(default_factory=dict) diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 09f9d122b98..25ef7af8d6e 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -49,6 +49,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttCommandTemplate, MqttValueTemplate +from .util import get_mqtt_data _LOGGER = logging.getLogger(__name__) @@ -222,7 +223,7 @@ class MqttNumber(MqttEntity, RestoreNumber): return self._current_number = num_value - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._config.get(CONF_STATE_TOPIC) is None: # Force into optimistic mode. diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index a6de0495690..12593550e2f 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -35,6 +35,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttCommandTemplate, MqttValueTemplate +from .util import get_mqtt_data _LOGGER = logging.getLogger(__name__) @@ -169,7 +170,7 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity): return self._attr_current_option = payload - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._config.get(CONF_STATE_TOPIC) is None: # Force into optimistic mode. diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 7c98fdf51b7..52ba1a7e3c2 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -46,7 +46,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttValueTemplate -from .util import valid_subscribe_topic +from .util import get_mqtt_data, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -300,7 +300,7 @@ class MqttSensor(MqttEntity, RestoreSensor): or self._config[CONF_LAST_RESET_TOPIC] == self._config[CONF_STATE_TOPIC] ): _update_last_reset(msg) - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) topics["state_topic"] = { "topic": self._config[CONF_STATE_TOPIC], @@ -314,7 +314,7 @@ class MqttSensor(MqttEntity, RestoreSensor): def last_reset_message_received(msg): """Handle new last_reset messages.""" _update_last_reset(msg) - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if ( CONF_LAST_RESET_TOPIC in self._config diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py index c8332046092..2ab226e44c0 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -54,6 +54,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttCommandTemplate, MqttValueTemplate +from .util import get_mqtt_data DEFAULT_NAME = "MQTT Siren" DEFAULT_PAYLOAD_ON = "ON" @@ -283,7 +284,7 @@ class MqttSiren(MqttEntity, SirenEntity): ) return self._update(process_turn_on_params(self, json_payload)) - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._config.get(CONF_STATE_TOPIC) is None: # Force into optimistic mode. diff --git a/homeassistant/components/mqtt/subscription.py b/homeassistant/components/mqtt/subscription.py index d0af533f294..05f7f3934ee 100644 --- a/homeassistant/components/mqtt/subscription.py +++ b/homeassistant/components/mqtt/subscription.py @@ -26,9 +26,12 @@ class EntitySubscription: qos: int = attr.ib(default=0) encoding: str = attr.ib(default="utf-8") - def resubscribe_if_necessary(self, hass, other): + def resubscribe_if_necessary( + self, hass: HomeAssistant, other: EntitySubscription | None + ) -> None: """Re-subscribe to the new topic if necessary.""" if not self._should_resubscribe(other): + assert other self.unsubscribe_callback = other.unsubscribe_callback return @@ -56,7 +59,7 @@ class EntitySubscription: return self.unsubscribe_callback = await self.subscribe_task - def _should_resubscribe(self, other): + def _should_resubscribe(self, other: EntitySubscription | None) -> bool: """Check if we should re-subscribe to the topic using the old state.""" if other is None: return True diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index af16b14bea1..f8bf2f5bc6a 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -47,6 +47,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttValueTemplate +from .util import get_mqtt_data DEFAULT_NAME = "MQTT Switch" DEFAULT_PAYLOAD_ON = "ON" @@ -168,7 +169,7 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): elif payload == PAYLOAD_NONE: self._state = None - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._config.get(CONF_STATE_TOPIC) is None: # Force into optimistic mode. diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index 6b957aded5c..09c4448fda7 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -20,7 +20,7 @@ from ..const import CONF_COMMAND_TOPIC, CONF_ENCODING, CONF_QOS, CONF_RETAIN from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, warn_for_legacy_schema from ..models import MqttValueTemplate -from ..util import valid_publish_topic +from ..util import get_mqtt_data, valid_publish_topic from .const import MQTT_VACUUM_ATTRIBUTES_BLOCKED from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services @@ -320,7 +320,7 @@ class MqttVacuum(MqttEntity, VacuumEntity): if fan_speed: self._fan_speed = fan_speed - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) topics_list = {topic for topic in self._state_topics.values() if topic} self._sub_state = subscription.async_prepare_subscribe_topics( diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index af6c8d289d8..8dfaba80109 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -32,7 +32,7 @@ from ..const import ( ) from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, warn_for_legacy_schema -from ..util import valid_publish_topic +from ..util import get_mqtt_data, valid_publish_topic from .const import MQTT_VACUUM_ATTRIBUTES_BLOCKED from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services @@ -211,7 +211,7 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): ) del payload[STATE] self._state_attrs.update(payload) - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._config.get(CONF_STATE_TOPIC): topics["state_position_topic"] = { diff --git a/tests/components/mqtt/test_mixins.py b/tests/components/mqtt/test_mixins.py new file mode 100644 index 00000000000..97a959b8dbe --- /dev/null +++ b/tests/components/mqtt/test_mixins.py @@ -0,0 +1,73 @@ +"""The tests for shared code of the MQTT platform.""" + +from unittest.mock import patch + +from homeassistant.components import mqtt, sensor +from homeassistant.const import EVENT_STATE_CHANGED, Platform +import homeassistant.core as ha +from homeassistant.setup import async_setup_component + +from tests.common import async_fire_mqtt_message + + +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SENSOR]) +async def test_availability_with_shared_state_topic( + hass, + mqtt_mock_entry_with_yaml_config, +): + """Test the state is not changed twice. + + When an entity with a shared state_topic and availability_topic becomes available + The state should only change once. + """ + assert await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "availability_topic": "test-topic", + "payload_available": True, + "payload_not_available": False, + "value_template": "{{ int(value) or '' }}", + "availability_template": "{{ value != '0' }}", + } + } + }, + ) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + events = [] + + @ha.callback + def callback(event): + events.append(event) + + hass.bus.async_listen(EVENT_STATE_CHANGED, callback) + + async_fire_mqtt_message(hass, "test-topic", "100") + await hass.async_block_till_done() + # Initially the state and the availability change + assert len(events) == 1 + + events.clear() + async_fire_mqtt_message(hass, "test-topic", "50") + await hass.async_block_till_done() + assert len(events) == 1 + + events.clear() + async_fire_mqtt_message(hass, "test-topic", "0") + await hass.async_block_till_done() + # Only the availability is changed since the template resukts in an empty payload + # This does not change the state + assert len(events) == 1 + + events.clear() + async_fire_mqtt_message(hass, "test-topic", "10") + await hass.async_block_till_done() + # The availability is changed but the topic is shared, + # hence there the state will be written when the value is updated + assert len(events) == 1 From cf6333463ba4cd446ebbba2b1e51ed392524fa0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Matheson=20Wergeland?= Date: Thu, 13 Oct 2022 11:40:47 +0200 Subject: [PATCH 02/13] Fix nobo_hub presenting temperature in zone with one decimal (#79743) Fix presenting temperature in zone with one decimal. Fix stepping the target temperatur without decimals. --- homeassistant/components/nobo_hub/climate.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/nobo_hub/climate.py b/homeassistant/components/nobo_hub/climate.py index a465bfa77ab..ba38e0b1530 100644 --- a/homeassistant/components/nobo_hub/climate.py +++ b/homeassistant/components/nobo_hub/climate.py @@ -1,7 +1,6 @@ """Python Control of Nobø Hub - Nobø Energy Control.""" from __future__ import annotations -import logging from typing import Any from pynobo import nobo @@ -24,7 +23,7 @@ from homeassistant.const import ( ATTR_NAME, ATTR_SUGGESTED_AREA, ATTR_VIA_DEVICE, - PRECISION_WHOLE, + PRECISION_TENTHS, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant, callback @@ -52,8 +51,6 @@ PRESET_MODES = [PRESET_NONE, PRESET_COMFORT, PRESET_ECO, PRESET_AWAY] MIN_TEMPERATURE = 7 MAX_TEMPERATURE = 40 -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry( hass: HomeAssistant, @@ -87,11 +84,12 @@ class NoboZone(ClimateEntity): _attr_max_temp = MAX_TEMPERATURE _attr_min_temp = MIN_TEMPERATURE - _attr_precision = PRECISION_WHOLE + _attr_precision = PRECISION_TENTHS _attr_preset_modes = PRESET_MODES - # Need to poll to get preset change when in HVACMode.AUTO. _attr_supported_features = SUPPORT_FLAGS _attr_temperature_unit = TEMP_CELSIUS + _attr_target_temperature_step = 1 + # Need to poll to get preset change when in HVACMode.AUTO, so can't set _attr_should_poll = False def __init__(self, zone_id, hub: nobo, override_type): """Initialize the climate device.""" From b988c5e591fe4045b825b2f1a3e23959fa8b0065 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 7 Oct 2022 12:17:31 +1300 Subject: [PATCH 03/13] Bump aioesphomeapi to 11.1.1 (#79762) --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 066050d796d..3ffceaae971 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==11.1.0"], + "requirements": ["aioesphomeapi==11.1.1"], "zeroconf": ["_esphomelib._tcp.local."], "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], diff --git a/requirements_all.txt b/requirements_all.txt index 857a8a0395a..62f6dbb84ef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -156,7 +156,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.1.0 +aioesphomeapi==11.1.1 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 49b17f021c1..583e5080d85 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -143,7 +143,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.1.0 +aioesphomeapi==11.1.1 # homeassistant.components.flo aioflo==2021.11.0 From 3311a092ea90db208092f9e3a8b7a31631370d10 Mon Sep 17 00:00:00 2001 From: hesselonline Date: Fri, 14 Oct 2022 13:09:00 +0200 Subject: [PATCH 04/13] Fix wallbox jwt issue (#79948) * Bump Wallbox package * remove debug message * Force update of auth token by emptying it first * Force token refresh by emptying token Improve exception handling * include tests * Update __init__.py * Removed the clearing ot jwt token, issue is fixed by upstream fix in wallbox package. * Catch connectionerror * Update homeassistant/components/wallbox/__init__.py Co-authored-by: Martin Hjelmare * Run black Co-authored-by: Martin Hjelmare --- homeassistant/components/wallbox/__init__.py | 11 +++++--- homeassistant/components/wallbox/lock.py | 3 +++ .../components/wallbox/manifest.json | 2 +- homeassistant/components/wallbox/number.py | 3 +++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/wallbox/__init__.py | 26 +++++++++++++++++++ tests/components/wallbox/test_lock.py | 13 ++++++++++ tests/components/wallbox/test_number.py | 19 +++++++++++++- 9 files changed, 73 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/wallbox/__init__.py b/homeassistant/components/wallbox/__init__.py index 9175f42827d..6382cf05940 100644 --- a/homeassistant/components/wallbox/__init__.py +++ b/homeassistant/components/wallbox/__init__.py @@ -17,6 +17,7 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, + UpdateFailed, ) from .const import ( @@ -93,6 +94,7 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Authenticate using Wallbox API.""" try: self._wallbox.authenticate() + except requests.exceptions.HTTPError as wallbox_connection_error: if wallbox_connection_error.response.status_code == HTTPStatus.FORBIDDEN: raise ConfigEntryAuthFailed from wallbox_connection_error @@ -125,11 +127,12 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]): data[CHARGER_STATUS_DESCRIPTION_KEY] = CHARGER_STATUS.get( data[CHARGER_STATUS_ID_KEY], ChargerStatus.UNKNOWN ) - return data - - except requests.exceptions.HTTPError as wallbox_connection_error: - raise ConnectionError from wallbox_connection_error + except ( + ConnectionError, + requests.exceptions.HTTPError, + ) as wallbox_connection_error: + raise UpdateFailed from wallbox_connection_error async def _async_update_data(self) -> dict[str, Any]: """Get new sensor data for Wallbox component.""" diff --git a/homeassistant/components/wallbox/lock.py b/homeassistant/components/wallbox/lock.py index d1b3c787735..7b5dca58010 100644 --- a/homeassistant/components/wallbox/lock.py +++ b/homeassistant/components/wallbox/lock.py @@ -6,6 +6,7 @@ from typing import Any from homeassistant.components.lock import LockEntity, LockEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import InvalidAuth, WallboxCoordinator, WallboxEntity @@ -36,6 +37,8 @@ async def async_setup_entry( ) except InvalidAuth: return + except ConnectionError as exc: + raise PlatformNotReady from exc async_add_entities( [ diff --git a/homeassistant/components/wallbox/manifest.json b/homeassistant/components/wallbox/manifest.json index 5c195b8bfce..433a759bea5 100644 --- a/homeassistant/components/wallbox/manifest.json +++ b/homeassistant/components/wallbox/manifest.json @@ -3,7 +3,7 @@ "name": "Wallbox", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wallbox", - "requirements": ["wallbox==0.4.9"], + "requirements": ["wallbox==0.4.10"], "codeowners": ["@hesselonline"], "iot_class": "cloud_polling", "loggers": ["wallbox"] diff --git a/homeassistant/components/wallbox/number.py b/homeassistant/components/wallbox/number.py index 5470ec11532..04db0ad9f7c 100644 --- a/homeassistant/components/wallbox/number.py +++ b/homeassistant/components/wallbox/number.py @@ -7,6 +7,7 @@ from typing import Optional, cast from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import InvalidAuth, WallboxCoordinator, WallboxEntity @@ -46,6 +47,8 @@ async def async_setup_entry( ) except InvalidAuth: return + except ConnectionError as exc: + raise PlatformNotReady from exc async_add_entities( [ diff --git a/requirements_all.txt b/requirements_all.txt index 62f6dbb84ef..d24aaf194af 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2503,7 +2503,7 @@ vultr==0.1.2 wakeonlan==2.1.0 # homeassistant.components.wallbox -wallbox==0.4.9 +wallbox==0.4.10 # homeassistant.components.waqi waqiasync==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 583e5080d85..4ad895ffa84 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1731,7 +1731,7 @@ vultr==0.1.2 wakeonlan==2.1.0 # homeassistant.components.wallbox -wallbox==0.4.9 +wallbox==0.4.10 # homeassistant.components.folder_watcher watchdog==2.1.9 diff --git a/tests/components/wallbox/__init__.py b/tests/components/wallbox/__init__.py index 8c979c42ebe..53af3b6383d 100644 --- a/tests/components/wallbox/__init__.py +++ b/tests/components/wallbox/__init__.py @@ -173,3 +173,29 @@ async def setup_integration_read_only(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() + + +async def setup_integration_platform_not_ready(hass: HomeAssistant) -> None: + """Test wallbox sensor class setup for read only.""" + + with requests_mock.Mocker() as mock_request: + mock_request.get( + "https://user-api.wall-box.com/users/signin", + json=authorisation_response, + status_code=HTTPStatus.OK, + ) + mock_request.get( + "https://api.wall-box.com/chargers/status/12345", + json=test_response, + status_code=HTTPStatus.OK, + ) + mock_request.put( + "https://api.wall-box.com/v2/charger/12345", + json=test_response, + status_code=HTTPStatus.NOT_FOUND, + ) + + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/wallbox/test_lock.py b/tests/components/wallbox/test_lock.py index 567d92757cd..bf0daa5c828 100644 --- a/tests/components/wallbox/test_lock.py +++ b/tests/components/wallbox/test_lock.py @@ -13,6 +13,7 @@ from . import ( authorisation_response, entry, setup_integration, + setup_integration_platform_not_ready, setup_integration_read_only, ) from .const import MOCK_LOCK_ENTITY_ID @@ -109,3 +110,15 @@ async def test_wallbox_lock_class_authentication_error(hass: HomeAssistant) -> N assert state is None await hass.config_entries.async_unload(entry.entry_id) + + +async def test_wallbox_lock_class_platform_not_ready(hass: HomeAssistant) -> None: + """Test wallbox lock not loaded on authentication error.""" + + await setup_integration_platform_not_ready(hass) + + state = hass.states.get(MOCK_LOCK_ENTITY_ID) + + assert state is None + + await hass.config_entries.async_unload(entry.entry_id) diff --git a/tests/components/wallbox/test_number.py b/tests/components/wallbox/test_number.py index 58e3450e6aa..c16c85d3696 100644 --- a/tests/components/wallbox/test_number.py +++ b/tests/components/wallbox/test_number.py @@ -9,7 +9,12 @@ from homeassistant.components.wallbox import CHARGER_MAX_CHARGING_CURRENT_KEY from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant -from . import authorisation_response, entry, setup_integration +from . import ( + authorisation_response, + entry, + setup_integration, + setup_integration_platform_not_ready, +) from .const import MOCK_NUMBER_ENTITY_ID @@ -71,3 +76,15 @@ async def test_wallbox_number_class_connection_error(hass: HomeAssistant) -> Non blocking=True, ) await hass.config_entries.async_unload(entry.entry_id) + + +async def test_wallbox_number_class_platform_not_ready(hass: HomeAssistant) -> None: + """Test wallbox lock not loaded on authentication error.""" + + await setup_integration_platform_not_ready(hass) + + state = hass.states.get(MOCK_NUMBER_ENTITY_ID) + + assert state is None + + await hass.config_entries.async_unload(entry.entry_id) From 75c2e213b29a1cc9891877826b3aebcca5a53342 Mon Sep 17 00:00:00 2001 From: Sean Vig Date: Tue, 11 Oct 2022 08:29:35 -0400 Subject: [PATCH 05/13] Fix audio detection for IP4m-1041 Amcrest camera (#80066) --- homeassistant/components/amcrest/__init__.py | 5 +- .../components/amcrest/binary_sensor.py | 50 +++++++++---------- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py index e3f48263e8b..8fea717e6bb 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -370,11 +370,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) ) event_codes = { - sensor.event_code + event_code for sensor in BINARY_SENSORS if sensor.key in binary_sensors and not sensor.should_poll - and sensor.event_code is not None + and sensor.event_codes is not None + for event_code in sensor.event_codes } _start_event_monitor(hass, name, api, event_codes) diff --git a/homeassistant/components/amcrest/binary_sensor.py b/homeassistant/components/amcrest/binary_sensor.py index e583aad904b..4d438c7c3bf 100644 --- a/homeassistant/components/amcrest/binary_sensor.py +++ b/homeassistant/components/amcrest/binary_sensor.py @@ -39,7 +39,7 @@ if TYPE_CHECKING: class AmcrestSensorEntityDescription(BinarySensorEntityDescription): """Describe Amcrest sensor entity.""" - event_code: str | None = None + event_codes: set[str] | None = None should_poll: bool = False @@ -51,7 +51,7 @@ _ONLINE_SCAN_INTERVAL = timedelta(seconds=60 - BINARY_SENSOR_SCAN_INTERVAL_SECS) _AUDIO_DETECTED_KEY = "audio_detected" _AUDIO_DETECTED_POLLED_KEY = "audio_detected_polled" _AUDIO_DETECTED_NAME = "Audio Detected" -_AUDIO_DETECTED_EVENT_CODE = "AudioMutation" +_AUDIO_DETECTED_EVENT_CODES = {"AudioMutation", "AudioIntensity"} _CROSSLINE_DETECTED_KEY = "crossline_detected" _CROSSLINE_DETECTED_POLLED_KEY = "crossline_detected_polled" @@ -70,39 +70,39 @@ BINARY_SENSORS: tuple[AmcrestSensorEntityDescription, ...] = ( key=_AUDIO_DETECTED_KEY, name=_AUDIO_DETECTED_NAME, device_class=BinarySensorDeviceClass.SOUND, - event_code=_AUDIO_DETECTED_EVENT_CODE, + event_codes=_AUDIO_DETECTED_EVENT_CODES, ), AmcrestSensorEntityDescription( key=_AUDIO_DETECTED_POLLED_KEY, name=_AUDIO_DETECTED_NAME, device_class=BinarySensorDeviceClass.SOUND, - event_code=_AUDIO_DETECTED_EVENT_CODE, + event_codes=_AUDIO_DETECTED_EVENT_CODES, should_poll=True, ), AmcrestSensorEntityDescription( key=_CROSSLINE_DETECTED_KEY, name=_CROSSLINE_DETECTED_NAME, device_class=BinarySensorDeviceClass.MOTION, - event_code=_CROSSLINE_DETECTED_EVENT_CODE, + event_codes={_CROSSLINE_DETECTED_EVENT_CODE}, ), AmcrestSensorEntityDescription( key=_CROSSLINE_DETECTED_POLLED_KEY, name=_CROSSLINE_DETECTED_NAME, device_class=BinarySensorDeviceClass.MOTION, - event_code=_CROSSLINE_DETECTED_EVENT_CODE, + event_codes={_CROSSLINE_DETECTED_EVENT_CODE}, should_poll=True, ), AmcrestSensorEntityDescription( key=_MOTION_DETECTED_KEY, name=_MOTION_DETECTED_NAME, device_class=BinarySensorDeviceClass.MOTION, - event_code=_MOTION_DETECTED_EVENT_CODE, + event_codes={_MOTION_DETECTED_EVENT_CODE}, ), AmcrestSensorEntityDescription( key=_MOTION_DETECTED_POLLED_KEY, name=_MOTION_DETECTED_NAME, device_class=BinarySensorDeviceClass.MOTION, - event_code=_MOTION_DETECTED_EVENT_CODE, + event_codes={_MOTION_DETECTED_EVENT_CODE}, should_poll=True, ), AmcrestSensorEntityDescription( @@ -211,13 +211,13 @@ class AmcrestBinarySensor(BinarySensorEntity): log_update_error(_LOGGER, "update", self.name, "binary sensor", error) return - if (event_code := self.entity_description.event_code) is None: - _LOGGER.error("Binary sensor %s event code not set", self.name) - return + if not (event_codes := self.entity_description.event_codes): + raise ValueError(f"Binary sensor {self.name} event codes not set") try: - self._attr_is_on = ( + self._attr_is_on = any( # type: ignore[arg-type] len(await self._api.async_event_channels_happened(event_code)) > 0 + for event_code in event_codes ) except AmcrestError as error: log_update_error(_LOGGER, "update", self.name, "binary sensor", error) @@ -266,17 +266,17 @@ class AmcrestBinarySensor(BinarySensorEntity): ) if ( - self.entity_description.event_code - and not self.entity_description.should_poll - ): - self.async_on_remove( - async_dispatcher_connect( - self.hass, - service_signal( - SERVICE_EVENT, - self._signal_name, - self.entity_description.event_code, - ), - self.async_event_received, + event_codes := self.entity_description.event_codes + ) and not self.entity_description.should_poll: + for event_code in event_codes: + self.async_on_remove( + async_dispatcher_connect( + self.hass, + service_signal( + SERVICE_EVENT, + self._signal_name, + event_code, + ), + self.async_event_received, + ) ) - ) From 5ab97e9f93784125e7be1907c0e343d92b9adba5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 11 Oct 2022 21:21:59 +0200 Subject: [PATCH 06/13] Fix set humidity in Tuya (#80132) --- homeassistant/components/tuya/humidifier.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/humidifier.py b/homeassistant/components/tuya/humidifier.py index 5fd33ba80d0..9891c81a456 100644 --- a/homeassistant/components/tuya/humidifier.py +++ b/homeassistant/components/tuya/humidifier.py @@ -104,7 +104,7 @@ class TuyaHumidifierEntity(TuyaEntity, HumidifierEntity): if int_type := self.find_dpcode( description.humidity, dptype=DPType.INTEGER, prefer_function=True ): - self._set_humiditye = int_type + self._set_humidity = int_type self._attr_min_humidity = int(int_type.min_scaled) self._attr_max_humidity = int(int_type.max_scaled) From b0d2f55f160672e954d0abffeaaa96f62164c6d2 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 12 Oct 2022 16:43:55 +0200 Subject: [PATCH 07/13] Correct units for sensors in nibe heatpump (#80140) --- homeassistant/components/nibe_heatpump/sensor.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nibe_heatpump/sensor.py b/homeassistant/components/nibe_heatpump/sensor.py index b0bc816dad6..66c66aaabe1 100644 --- a/homeassistant/components/nibe_heatpump/sensor.py +++ b/homeassistant/components/nibe_heatpump/sensor.py @@ -19,6 +19,7 @@ from homeassistant.const import ( ENERGY_KILO_WATT_HOUR, ENERGY_MEGA_WATT_HOUR, ENERGY_WATT_HOUR, + POWER_WATT, TEMP_CELSIUS, TEMP_FAHRENHEIT, TIME_HOURS, @@ -72,24 +73,31 @@ UNIT_DESCRIPTIONS = { state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_POTENTIAL_MILLIVOLT, ), + "W": SensorEntityDescription( + key="W", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=POWER_WATT, + ), "Wh": SensorEntityDescription( key="Wh", entity_category=EntityCategory.DIAGNOSTIC, - device_class=SensorDeviceClass.POWER, + device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=ENERGY_WATT_HOUR, ), "kWh": SensorEntityDescription( key="kWh", entity_category=EntityCategory.DIAGNOSTIC, - device_class=SensorDeviceClass.POWER, + device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), "MWh": SensorEntityDescription( key="MWh", entity_category=EntityCategory.DIAGNOSTIC, - device_class=SensorDeviceClass.POWER, + device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=ENERGY_MEGA_WATT_HOUR, ), @@ -97,6 +105,7 @@ UNIT_DESCRIPTIONS = { key="h", entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=TIME_HOURS, ), } @@ -133,6 +142,7 @@ class Sensor(CoilEntity, SensorEntity): self.entity_description = entity_description else: self._attr_native_unit_of_measurement = coil.unit + self._attr_entity_category = EntityCategory.DIAGNOSTIC def _async_read_coil(self, coil: Coil): self._attr_native_value = coil.value From 3451d150ae68eadebf0d0518f9c43dd41904c18d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Oct 2022 19:30:09 -1000 Subject: [PATCH 08/13] Bump yalexs to 1.2.6 (#80142) --- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index a816ddc06ff..b7dde070049 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,7 +2,7 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": ["yalexs==1.2.4"], + "requirements": ["yalexs==1.2.6"], "codeowners": ["@bdraco"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index d24aaf194af..c41973e8bd8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2568,7 +2568,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==1.9.2 # homeassistant.components.august -yalexs==1.2.4 +yalexs==1.2.6 # homeassistant.components.yeelight yeelight==0.7.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4ad895ffa84..91437c7afa3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1778,7 +1778,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==1.9.2 # homeassistant.components.august -yalexs==1.2.4 +yalexs==1.2.6 # homeassistant.components.yeelight yeelight==0.7.10 From 8dfd8491a721d682e22f078b8fd81a86344b1873 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 13 Oct 2022 00:06:23 +0200 Subject: [PATCH 09/13] Fix incorrect deprecation year for conversion utils (#80195) Fix incorrect depr year --- homeassistant/util/distance.py | 2 +- homeassistant/util/pressure.py | 2 +- homeassistant/util/speed.py | 2 +- homeassistant/util/temperature.py | 2 +- homeassistant/util/volume.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/util/distance.py b/homeassistant/util/distance.py index f5dbeaf42d5..719379d4c61 100644 --- a/homeassistant/util/distance.py +++ b/homeassistant/util/distance.py @@ -48,7 +48,7 @@ def convert(value: float, from_unit: str, to_unit: str) -> float: """Convert one unit of measurement to another.""" report( "uses distance utility. This is deprecated since 2022.10 and will " - "stop working in Home Assistant 2022.4, it should be updated to use " + "stop working in Home Assistant 2023.4, it should be updated to use " "unit_conversion.DistanceConverter instead", error_if_core=False, ) diff --git a/homeassistant/util/pressure.py b/homeassistant/util/pressure.py index 2a8e20ed025..d6d0c79741f 100644 --- a/homeassistant/util/pressure.py +++ b/homeassistant/util/pressure.py @@ -27,7 +27,7 @@ def convert(value: float, from_unit: str, to_unit: str) -> float: """Convert one unit of measurement to another.""" report( "uses pressure utility. This is deprecated since 2022.10 and will " - "stop working in Home Assistant 2022.4, it should be updated to use " + "stop working in Home Assistant 2023.4, it should be updated to use " "unit_conversion.PressureConverter instead", error_if_core=False, ) diff --git a/homeassistant/util/speed.py b/homeassistant/util/speed.py index 76ea873d7fe..f531e2d78f7 100644 --- a/homeassistant/util/speed.py +++ b/homeassistant/util/speed.py @@ -34,7 +34,7 @@ def convert(value: float, from_unit: str, to_unit: str) -> float: """Convert one unit of measurement to another.""" report( "uses speed utility. This is deprecated since 2022.10 and will " - "stop working in Home Assistant 2022.4, it should be updated to use " + "stop working in Home Assistant 2023.4, it should be updated to use " "unit_conversion.SpeedConverter instead", error_if_core=False, ) diff --git a/homeassistant/util/temperature.py b/homeassistant/util/temperature.py index 9173fbc5eee..0c2608eb4b5 100644 --- a/homeassistant/util/temperature.py +++ b/homeassistant/util/temperature.py @@ -39,7 +39,7 @@ def convert( """Convert a temperature from one unit to another.""" report( "uses temperature utility. This is deprecated since 2022.10 and will " - "stop working in Home Assistant 2022.4, it should be updated to use " + "stop working in Home Assistant 2023.4, it should be updated to use " "unit_conversion.TemperatureConverter instead", error_if_core=False, ) diff --git a/homeassistant/util/volume.py b/homeassistant/util/volume.py index b468b9e6e0d..e21cebd2982 100644 --- a/homeassistant/util/volume.py +++ b/homeassistant/util/volume.py @@ -42,7 +42,7 @@ def convert(volume: float, from_unit: str, to_unit: str) -> float: """Convert a volume from one unit to another.""" report( "uses volume utility. This is deprecated since 2022.10 and will " - "stop working in Home Assistant 2022.4, it should be updated to use " + "stop working in Home Assistant 2023.4, it should be updated to use " "unit_conversion.VolumeConverter instead", error_if_core=False, ) From a5f9cd2732a28cf31cd8b76243899c5f15229753 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 13 Oct 2022 14:53:09 -1000 Subject: [PATCH 10/13] Fix nexia permanent hold when cool and heat temps are within 2 degrees (#80297) --- homeassistant/components/nexia/climate.py | 4 ++-- homeassistant/components/nexia/manifest.json | 2 +- homeassistant/components/nexia/switch.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py index 81e6158a872..66c325d2fc3 100644 --- a/homeassistant/components/nexia/climate.py +++ b/homeassistant/components/nexia/climate.py @@ -206,7 +206,7 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): """Set the hvac run mode.""" if run_mode is not None: if run_mode == HOLD_PERMANENT: - await self._zone.call_permanent_hold() + await self._zone.set_permanent_hold() else: await self._zone.call_return_to_schedule() if hvac_mode is not None: @@ -399,7 +399,7 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): await self._zone.call_return_to_schedule() await self._zone.set_mode(mode=OPERATION_MODE_AUTO) else: - await self._zone.call_permanent_hold() + await self._zone.set_permanent_hold() await self._zone.set_mode(mode=HA_TO_NEXIA_HVAC_MODE_MAP[hvac_mode]) self._signal_zone_update() diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index e381dc95897..77280b1f503 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -1,7 +1,7 @@ { "domain": "nexia", "name": "Nexia/American Standard/Trane", - "requirements": ["nexia==2.0.2"], + "requirements": ["nexia==2.0.4"], "codeowners": ["@bdraco"], "documentation": "https://www.home-assistant.io/integrations/nexia", "config_flow": true, diff --git a/homeassistant/components/nexia/switch.py b/homeassistant/components/nexia/switch.py index e242032c947..643a4d585c4 100644 --- a/homeassistant/components/nexia/switch.py +++ b/homeassistant/components/nexia/switch.py @@ -62,7 +62,7 @@ class NexiaHoldSwitch(NexiaThermostatZoneEntity, SwitchEntity): if self._zone.get_current_mode() == OPERATION_MODE_OFF: await self._zone.call_permanent_off() else: - await self._zone.call_permanent_hold() + await self._zone.set_permanent_hold() self._signal_zone_update() async def async_turn_off(self, **kwargs: Any) -> None: diff --git a/requirements_all.txt b/requirements_all.txt index c41973e8bd8..371ec7e2288 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1132,7 +1132,7 @@ nettigo-air-monitor==1.4.2 neurio==0.3.1 # homeassistant.components.nexia -nexia==2.0.2 +nexia==2.0.4 # homeassistant.components.nextcloud nextcloudmonitor==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 91437c7afa3..fafbb93568d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -822,7 +822,7 @@ netmap==0.7.0.2 nettigo-air-monitor==1.4.2 # homeassistant.components.nexia -nexia==2.0.2 +nexia==2.0.4 # homeassistant.components.discord nextcord==2.0.0a8 From 6f00f42cc6f24a6714cbeae77d193eac2a639c4c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 13 Oct 2022 20:51:27 -1000 Subject: [PATCH 11/13] Bump HAP-python to fix pairing with iOS 16 (#80301) Using the ha- fork until upstream can pickup and merge pending PRs. The plan is to revert back to upstream HAP-python when its back in sync Fixes #79305 Fixes #79304 --- homeassistant/components/homekit/manifest.json | 2 +- requirements_all.txt | 6 +++--- requirements_test_all.txt | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index 2ca78b6d915..187eaf4c869 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit", "documentation": "https://www.home-assistant.io/integrations/homekit", "requirements": [ - "HAP-python==4.5.0", + "ha-HAP-python==4.5.2", "fnvhash==0.1.0", "PyQRCode==1.2.1", "base36==0.1.1" diff --git a/requirements_all.txt b/requirements_all.txt index 371ec7e2288..de488c27520 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -10,9 +10,6 @@ AIOAladdinConnect==0.1.46 # homeassistant.components.adax Adax-local==0.1.4 -# homeassistant.components.homekit -HAP-python==4.5.0 - # homeassistant.components.mastodon Mastodon.py==1.5.1 @@ -815,6 +812,9 @@ gstreamer-player==1.1.2 # homeassistant.components.profiler guppy3==3.1.2 +# homeassistant.components.homekit +ha-HAP-python==4.5.2 + # homeassistant.components.generic # homeassistant.components.stream ha-av==10.0.0b5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fafbb93568d..93e564bee30 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -12,9 +12,6 @@ AIOAladdinConnect==0.1.46 # homeassistant.components.adax Adax-local==0.1.4 -# homeassistant.components.homekit -HAP-python==4.5.0 - # homeassistant.components.flick_electric PyFlick==0.0.2 @@ -607,6 +604,9 @@ gspread==5.5.0 # homeassistant.components.profiler guppy3==3.1.2 +# homeassistant.components.homekit +ha-HAP-python==4.5.2 + # homeassistant.components.generic # homeassistant.components.stream ha-av==10.0.0b5 From e6e531e20cbc950c754963214b6685e3073dd9fa Mon Sep 17 00:00:00 2001 From: Austin Mroczek Date: Fri, 14 Oct 2022 08:11:09 -0700 Subject: [PATCH 12/13] Bump total_connect_client to 2022.10 (#80331) --- homeassistant/components/totalconnect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/totalconnect/manifest.json b/homeassistant/components/totalconnect/manifest.json index 461bff0cfd0..71c11958d40 100644 --- a/homeassistant/components/totalconnect/manifest.json +++ b/homeassistant/components/totalconnect/manifest.json @@ -2,7 +2,7 @@ "domain": "totalconnect", "name": "Total Connect", "documentation": "https://www.home-assistant.io/integrations/totalconnect", - "requirements": ["total_connect_client==2022.5"], + "requirements": ["total_connect_client==2022.10"], "dependencies": [], "codeowners": ["@austinmroczek"], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index de488c27520..e1a1911b95f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2416,7 +2416,7 @@ tololib==0.1.0b3 toonapi==0.2.1 # homeassistant.components.totalconnect -total_connect_client==2022.5 +total_connect_client==2022.10 # homeassistant.components.tplink_lte tp-connected==0.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 93e564bee30..628e8f869cb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1656,7 +1656,7 @@ tololib==0.1.0b3 toonapi==0.2.1 # homeassistant.components.totalconnect -total_connect_client==2022.5 +total_connect_client==2022.10 # homeassistant.components.transmission transmissionrpc==0.11 From e8c8a492680891a9bc155af6a0606aee98debed6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 14 Oct 2022 12:04:20 -0400 Subject: [PATCH 13/13] Bumped version to 2022.10.4 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 012a633947d..b60ab0d17e9 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 10 -PATCH_VERSION: Final = "3" +PATCH_VERSION: Final = "4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 8b6d4707521..9a6c1458742 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.10.3" +version = "2022.10.4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst"