Files
core/tests/components/tasmota/test_camera.py
anishsane a12617645b Add support for Tasmota camera (#144067)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-09-10 08:13:48 +02:00

309 lines
9.1 KiB
Python

"""The tests for the Tasmota camera platform."""
from asyncio import Future
import copy
import json
from unittest.mock import patch
import pytest
from homeassistant.components.camera import CameraState
from homeassistant.components.tasmota.const import DEFAULT_PREFIX
from homeassistant.const import ATTR_ASSUMED_STATE, Platform
from homeassistant.core import HomeAssistant
from .test_common import (
DEFAULT_CONFIG,
help_test_availability,
help_test_availability_discovery_update,
help_test_availability_poll_state,
help_test_availability_when_connection_lost,
help_test_deep_sleep_availability,
help_test_deep_sleep_availability_when_connection_lost,
help_test_discovery_device_remove,
help_test_discovery_removal,
help_test_discovery_update_unchanged,
help_test_entity_id_update_discovery_update,
)
from tests.common import async_fire_mqtt_message
from tests.typing import ClientSessionGenerator, MqttMockHAClient, MqttMockPahoClient
SMALLEST_VALID_JPEG = (
"ffd8ffe000104a46494600010101004800480000ffdb00430003020202020203020202030303030406040404040408060"
"6050609080a0a090809090a0c0f0c0a0b0e0b09090d110d0e0f101011100a0c12131210130f101010ffc9000b08000100"
"0101011100ffcc000600101005ffda0008010100003f00d2cf20ffd9"
)
SMALLEST_VALID_JPEG_BYTES = bytes.fromhex(SMALLEST_VALID_JPEG)
async def test_controlling_state_via_mqtt(
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota
) -> None:
"""Test state update via MQTT."""
config = copy.deepcopy(DEFAULT_CONFIG)
config["cam"] = 1
mac = config["mac"]
async_fire_mqtt_message(
hass,
f"{DEFAULT_PREFIX}/{mac}/config",
json.dumps(config),
)
await hass.async_block_till_done()
state = hass.states.get("camera.tasmota")
assert state.state == "unavailable"
assert not state.attributes.get(ATTR_ASSUMED_STATE)
async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online")
await hass.async_block_till_done()
state = hass.states.get("camera.tasmota")
assert state.state == CameraState.IDLE
assert not state.attributes.get(ATTR_ASSUMED_STATE)
async def test_availability_when_connection_lost(
hass: HomeAssistant,
mqtt_client_mock: MqttMockPahoClient,
mqtt_mock: MqttMockHAClient,
setup_tasmota,
) -> None:
"""Test availability after MQTT disconnection."""
config = copy.deepcopy(DEFAULT_CONFIG)
config["cam"] = 1
await help_test_availability_when_connection_lost(
hass, mqtt_client_mock, mqtt_mock, Platform.CAMERA, config, object_id="tasmota"
)
async def test_deep_sleep_availability_when_connection_lost(
hass: HomeAssistant,
mqtt_client_mock: MqttMockPahoClient,
mqtt_mock: MqttMockHAClient,
setup_tasmota,
) -> None:
"""Test availability after MQTT disconnection."""
config = copy.deepcopy(DEFAULT_CONFIG)
config["cam"] = 1
await help_test_deep_sleep_availability_when_connection_lost(
hass, mqtt_client_mock, mqtt_mock, Platform.CAMERA, config, object_id="tasmota"
)
async def test_availability(
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota
) -> None:
"""Test availability."""
config = copy.deepcopy(DEFAULT_CONFIG)
config["cam"] = 1
await help_test_availability(
hass, mqtt_mock, Platform.CAMERA, config, object_id="tasmota"
)
async def test_deep_sleep_availability(
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota
) -> None:
"""Test availability."""
config = copy.deepcopy(DEFAULT_CONFIG)
config["cam"] = 1
await help_test_deep_sleep_availability(
hass, mqtt_mock, Platform.CAMERA, config, object_id="tasmota"
)
async def test_availability_discovery_update(
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota
) -> None:
"""Test availability discovery update."""
config = copy.deepcopy(DEFAULT_CONFIG)
config["cam"] = 1
await help_test_availability_discovery_update(
hass, mqtt_mock, Platform.CAMERA, config, object_id="tasmota"
)
async def test_availability_poll_state(
hass: HomeAssistant,
mqtt_client_mock: MqttMockPahoClient,
mqtt_mock: MqttMockHAClient,
setup_tasmota,
) -> None:
"""Test polling after MQTT connection (re)established."""
config = copy.deepcopy(DEFAULT_CONFIG)
config["cam"] = 1
poll_topic = "tasmota_49A3BC/cmnd/STATE"
await help_test_availability_poll_state(
hass, mqtt_client_mock, mqtt_mock, Platform.CAMERA, config, poll_topic, ""
)
async def test_discovery_removal_camera(
hass: HomeAssistant,
mqtt_mock: MqttMockHAClient,
caplog: pytest.LogCaptureFixture,
setup_tasmota,
) -> None:
"""Test removal of discovered camera."""
config1 = copy.deepcopy(DEFAULT_CONFIG)
config1["cam"] = 1
config2 = copy.deepcopy(DEFAULT_CONFIG)
config2["cam"] = 0
await help_test_discovery_removal(
hass,
mqtt_mock,
caplog,
Platform.CAMERA,
config1,
config2,
object_id="tasmota",
name="Tasmota",
)
async def test_discovery_update_unchanged_camera(
hass: HomeAssistant,
mqtt_mock: MqttMockHAClient,
caplog: pytest.LogCaptureFixture,
setup_tasmota,
) -> None:
"""Test update of discovered camera."""
config = copy.deepcopy(DEFAULT_CONFIG)
config["cam"] = 1
with patch(
"homeassistant.components.tasmota.camera.TasmotaCamera.discovery_update"
) as discovery_update:
await help_test_discovery_update_unchanged(
hass,
mqtt_mock,
caplog,
Platform.CAMERA,
config,
discovery_update,
object_id="tasmota",
name="Tasmota",
)
async def test_discovery_device_remove(
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota
) -> None:
"""Test device registry remove."""
config = copy.deepcopy(DEFAULT_CONFIG)
config["cam"] = 1
unique_id = f"{DEFAULT_CONFIG['mac']}_camera_camera_0"
await help_test_discovery_device_remove(
hass, mqtt_mock, Platform.CAMERA, unique_id, config
)
async def test_entity_id_update_discovery_update(
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota
) -> None:
"""Test MQTT discovery update when entity_id is updated."""
config = copy.deepcopy(DEFAULT_CONFIG)
config["cam"] = 1
await help_test_entity_id_update_discovery_update(
hass, mqtt_mock, Platform.CAMERA, config, object_id="tasmota"
)
async def test_camera_single_frame(
hass: HomeAssistant,
mqtt_mock: MqttMockHAClient,
setup_tasmota,
hass_client: ClientSessionGenerator,
) -> None:
"""Test single frame capture."""
class MockClientResponse:
def __init__(self, text) -> None:
self._text = text
async def read(self):
return self._text
config = copy.deepcopy(DEFAULT_CONFIG)
config["cam"] = 1
mac = config["mac"]
async_fire_mqtt_message(
hass,
f"{DEFAULT_PREFIX}/{mac}/config",
json.dumps(config),
)
mock_single_image_stream = Future()
mock_single_image_stream.set_result(MockClientResponse(SMALLEST_VALID_JPEG_BYTES))
with patch(
"hatasmota.camera.TasmotaCamera.get_still_image_stream",
return_value=mock_single_image_stream,
):
client = await hass_client()
resp = await client.get("/api/camera_proxy/camera.tasmota")
await hass.async_block_till_done()
assert resp.status == 200
assert resp.content_type == "image/jpeg"
assert resp.content_length == len(SMALLEST_VALID_JPEG_BYTES)
assert await resp.read() == SMALLEST_VALID_JPEG_BYTES
async def test_camera_stream(
hass: HomeAssistant,
mqtt_mock: MqttMockHAClient,
setup_tasmota,
hass_client: ClientSessionGenerator,
) -> None:
"""Test mjpeg stream capture."""
class MockClientResponse:
def __init__(self, text) -> None:
self._text = text
self._frame_available = True
async def read(self, buffer_size):
if self._frame_available:
self._frame_available = False
return self._text
return None
def close(self):
pass
@property
def headers(self):
return {"Content-Type": "multipart/x-mixed-replace"}
@property
def content(self):
return self
config = copy.deepcopy(DEFAULT_CONFIG)
config["cam"] = 1
mac = config["mac"]
async_fire_mqtt_message(
hass,
f"{DEFAULT_PREFIX}/{mac}/config",
json.dumps(config),
)
mock_mjpeg_stream = Future()
mock_mjpeg_stream.set_result(MockClientResponse(SMALLEST_VALID_JPEG_BYTES))
with patch(
"hatasmota.camera.TasmotaCamera.get_mjpeg_stream",
return_value=mock_mjpeg_stream,
):
client = await hass_client()
resp = await client.get("/api/camera_proxy_stream/camera.tasmota")
await hass.async_block_till_done()
assert resp.status == 200
assert resp.content_type == "multipart/x-mixed-replace"
assert await resp.read() == SMALLEST_VALID_JPEG_BYTES