Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 23 additions & 12 deletions homeassistant/components/mqtt/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,16 +389,6 @@ def _async_setup_entities() -> None:
_async_setup_entities()


def init_entity_id_from_config(
hass: HomeAssistant, entity: Entity, config: ConfigType, entity_id_format: str
) -> None:
"""Set entity_id from object_id if defined in config."""
if CONF_OBJECT_ID in config:
entity.entity_id = async_generate_entity_id(
entity_id_format, config[CONF_OBJECT_ID], None, hass
)


class MqttAttributesMixin(Entity):
"""Mixin used for platforms that support JSON attributes."""

Expand Down Expand Up @@ -1312,6 +1302,7 @@ class MqttEntity(
_attr_should_poll = False
_default_name: str | None
_entity_id_format: str
_update_registry_entity_id: str | None = None

def __init__(
self,
Expand Down Expand Up @@ -1346,13 +1337,33 @@ def __init__(

def _init_entity_id(self) -> None:
"""Set entity_id from object_id if defined in config."""
init_entity_id_from_config(
self.hass, self, self._config, self._entity_id_format
if CONF_OBJECT_ID not in self._config:
return
self.entity_id = async_generate_entity_id(
self._entity_id_format, self._config[CONF_OBJECT_ID], None, self.hass
)
if self.unique_id is None:
return
# Check for previous deleted entities
entity_registry = er.async_get(self.hass)
entity_platform = self._entity_id_format.split(".")[0]
if (
deleted_entry := entity_registry.deleted_entities.get(
(entity_platform, DOMAIN, self.unique_id)
)
) and deleted_entry.entity_id != self.entity_id:
# Plan to update the entity_id basis on `object_id` if a deleted entity was found
self._update_registry_entity_id = self.entity_id

@final
async def async_added_to_hass(self) -> None:
"""Subscribe to MQTT events."""
if self._update_registry_entity_id is not None:
entity_registry = er.async_get(self.hass)
entity_registry.async_update_entity(
self.entity_id, new_entity_id=self._update_registry_entity_id
)

await super().async_added_to_hass()
self._subscriptions = {}
self._prepare_subscribe_topics()
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/rest_command/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,11 @@ async def async_service_handler(service: ServiceCall) -> ServiceResponse:
)

if not service.return_response:
# always read the response to avoid closing the connection
# before the server has finished sending it, while avoiding excessive memory usage
async for _ in response.content.iter_chunked(1024):
pass

return None

_content = None
Expand Down
7 changes: 7 additions & 0 deletions homeassistant/components/tessie/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,13 @@ def update_percentage(self) -> int | None:
return self.get("vehicle_state_software_update_install_perc")
return None

@property
def release_url(self) -> str | None:
"""URL to the full release notes of the latest version available."""
if self.latest_version is None:
return None
return f"https://stats.tessie.com/versions/{self.latest_version}"

async def async_install(
self, version: str | None, backup: bool, **kwargs: Any
) -> None:
Expand Down
8 changes: 6 additions & 2 deletions homeassistant/components/yolink/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
ATTR_DEVICE_LEAK_SENSOR,
ATTR_DEVICE_MOTION_SENSOR,
ATTR_DEVICE_MULTI_WATER_METER_CONTROLLER,
ATTR_DEVICE_SMOKE_ALARM,
ATTR_DEVICE_VIBRATION_SENSOR,
ATTR_DEVICE_WATER_METER_CONTROLLER,
)
Expand Down Expand Up @@ -53,6 +54,7 @@ class YoLinkBinarySensorEntityDescription(BinarySensorEntityDescription):
ATTR_DEVICE_CO_SMOKE_SENSOR,
ATTR_DEVICE_WATER_METER_CONTROLLER,
ATTR_DEVICE_MULTI_WATER_METER_CONTROLLER,
ATTR_DEVICE_SMOKE_ALARM,
]


Expand Down Expand Up @@ -90,8 +92,10 @@ class YoLinkBinarySensorEntityDescription(BinarySensorEntityDescription):
YoLinkBinarySensorEntityDescription(
key="smoke_detected",
device_class=BinarySensorDeviceClass.SMOKE,
value=lambda state: state.get("smokeAlarm"),
exists_fn=lambda device: device.device_type == ATTR_DEVICE_CO_SMOKE_SENSOR,
value=lambda state: state.get("smokeAlarm") is True
or state.get("denseSmokeAlarm") is True,
exists_fn=lambda device: device.device_type
in [ATTR_DEVICE_CO_SMOKE_SENSOR, ATTR_DEVICE_SMOKE_ALARM],
),
YoLinkBinarySensorEntityDescription(
key="pipe_leak_detected",
Expand Down
4 changes: 4 additions & 0 deletions homeassistant/components/yolink/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
ATTR_DEVICE_POWER_FAILURE_ALARM,
ATTR_DEVICE_SIREN,
ATTR_DEVICE_SMART_REMOTER,
ATTR_DEVICE_SMOKE_ALARM,
ATTR_DEVICE_SOIL_TH_SENSOR,
ATTR_DEVICE_SWITCH,
ATTR_DEVICE_TH_SENSOR,
Expand Down Expand Up @@ -106,6 +107,7 @@ class YoLinkSensorEntityDescription(SensorEntityDescription):
ATTR_DEVICE_CO_SMOKE_SENSOR,
ATTR_GARAGE_DOOR_CONTROLLER,
ATTR_DEVICE_SOIL_TH_SENSOR,
ATTR_DEVICE_SMOKE_ALARM,
]

BATTERY_POWER_SENSOR = [
Expand All @@ -126,12 +128,14 @@ class YoLinkSensorEntityDescription(SensorEntityDescription):
ATTR_DEVICE_WATER_METER_CONTROLLER,
ATTR_DEVICE_MULTI_WATER_METER_CONTROLLER,
ATTR_DEVICE_SOIL_TH_SENSOR,
ATTR_DEVICE_SMOKE_ALARM,
]

MCU_DEV_TEMPERATURE_SENSOR = [
ATTR_DEVICE_LEAK_SENSOR,
ATTR_DEVICE_MOTION_SENSOR,
ATTR_DEVICE_CO_SMOKE_SENSOR,
ATTR_DEVICE_SMOKE_ALARM,
]

NONE_HUMIDITY_SENSOR_MODELS = [
Expand Down
46 changes: 46 additions & 0 deletions tests/components/mqtt/test_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -1496,6 +1496,52 @@ async def test_discovery_with_object_id(
assert (domain, "object bla") in hass.data["mqtt"].discovery_already_discovered


async def test_discovery_with_object_id_for_previous_deleted_entity(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
) -> None:
"""Test discovering an MQTT entity with object_id and unique_id."""

topic = "homeassistant/sensor/object/bla/config"
config = (
'{ "name": "Hello World 11", "unique_id": "very_unique", '
'"obj_id": "hello_id", "state_topic": "test-topic" }'
)
new_config = (
'{ "name": "Hello World 11", "unique_id": "very_unique", '
'"obj_id": "updated_hello_id", "state_topic": "test-topic" }'
)
initial_entity_id = "sensor.hello_id"
new_entity_id = "sensor.updated_hello_id"
name = "Hello World 11"
domain = "sensor"

await mqtt_mock_entry()
async_fire_mqtt_message(hass, topic, config)
await hass.async_block_till_done()

state = hass.states.get(initial_entity_id)

assert state is not None
assert state.name == name
assert (domain, "object bla") in hass.data["mqtt"].discovery_already_discovered

# Delete the entity
async_fire_mqtt_message(hass, topic, "")
await hass.async_block_till_done()
assert (domain, "object bla") not in hass.data["mqtt"].discovery_already_discovered

# Rediscover with new object_id
async_fire_mqtt_message(hass, topic, new_config)
await hass.async_block_till_done()

state = hass.states.get(new_entity_id)

assert state is not None
assert state.name == name
assert (domain, "object bla") in hass.data["mqtt"].discovery_already_discovered


async def test_discovery_incl_nodeid(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None:
Expand Down
26 changes: 25 additions & 1 deletion tests/components/rest_command/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ async def test_rest_command_get_response_malformed_json(

aioclient_mock.get(
TEST_URL,
content='{"status": "failure", 42',
content=b'{"status": "failure", 42',
headers={"content-type": "application/json"},
)

Expand Down Expand Up @@ -381,3 +381,27 @@ async def test_rest_command_get_response_none(
)

assert not response


async def test_rest_command_response_iter_chunked(
hass: HomeAssistant,
setup_component: ComponentSetup,
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Ensure response is consumed when return_response is False."""
await setup_component()

png = base64.decodebytes(
b"iVBORw0KGgoAAAANSUhEUgAAAAIAAAABCAIAAAB7QOjdAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQ"
b"UAAAAJcEhZcwAAFiUAABYlAUlSJPAAAAAPSURBVBhXY/h/ku////8AECAE1JZPvDAAAAAASUVORK5CYII="
)
aioclient_mock.get(TEST_URL, content=png)

with patch("aiohttp.StreamReader.iter_chunked", autospec=True) as mock_iter_chunked:
response = await hass.services.async_call(DOMAIN, "get_test", {}, blocking=True)

# Ensure the response is not returned
assert response is None

# Verify iter_chunked was called with a chunk size
assert mock_iter_chunked.called
2 changes: 1 addition & 1 deletion tests/components/tessie/snapshots/test_update.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
'installed_version': '2023.38.6',
'latest_version': '2023.44.30.4',
'release_summary': None,
'release_url': None,
'release_url': 'https://stats.tessie.com/versions/2023.44.30.4',
'skipped_version': None,
'supported_features': <UpdateEntityFeature: 5>,
'title': None,
Expand Down
Loading