1
0
mirror of https://github.com/home-assistant/core.git synced 2026-04-02 00:20:30 +01:00

Add TRMNL integration (#165499)

This commit is contained in:
Joost Lekkerkerker
2026-03-14 16:17:19 +01:00
committed by GitHub
parent 54f96bcc33
commit 9d2febd24e
25 changed files with 820 additions and 0 deletions

View File

@@ -570,6 +570,7 @@ homeassistant.components.trafikverket_train.*
homeassistant.components.trafikverket_weatherstation.*
homeassistant.components.transmission.*
homeassistant.components.trend.*
homeassistant.components.trmnl.*
homeassistant.components.tts.*
homeassistant.components.twentemilieu.*
homeassistant.components.unifi.*

2
CODEOWNERS generated
View File

@@ -1770,6 +1770,8 @@ build.json @home-assistant/supervisor
/tests/components/trend/ @jpbede
/homeassistant/components/triggercmd/ @rvmey
/tests/components/triggercmd/ @rvmey
/homeassistant/components/trmnl/ @joostlek
/tests/components/trmnl/ @joostlek
/homeassistant/components/tts/ @home-assistant/core
/tests/components/tts/ @home-assistant/core
/homeassistant/components/tuya/ @Tuya @zlinoliver

View File

@@ -0,0 +1,29 @@
"""The TRMNL integration."""
from __future__ import annotations
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from .coordinator import TRMNLConfigEntry, TRMNLCoordinator
PLATFORMS: list[Platform] = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: TRMNLConfigEntry) -> bool:
"""Set up TRMNL from a config entry."""
coordinator = TRMNLCoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: TRMNLConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -0,0 +1,49 @@
"""Config flow for TRMNL."""
from __future__ import annotations
from typing import Any
from trmnl import TRMNLClient
from trmnl.exceptions import TRMNLAuthenticationError, TRMNLError
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_API_KEY
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN, LOGGER
class TRMNLConfigFlow(ConfigFlow, domain=DOMAIN):
"""TRMNL config flow."""
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a flow initialized by the user."""
errors: dict[str, str] = {}
if user_input:
session = async_get_clientsession(self.hass)
client = TRMNLClient(token=user_input[CONF_API_KEY], session=session)
try:
user = await client.get_me()
except TRMNLAuthenticationError:
errors["base"] = "invalid_auth"
except TRMNLError:
errors["base"] = "cannot_connect"
except Exception: # noqa: BLE001
LOGGER.exception("Unexpected error")
errors["base"] = "unknown"
else:
await self.async_set_unique_id(str(user.identifier))
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=user.name,
data={CONF_API_KEY: user_input[CONF_API_KEY]},
)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema({vol.Required(CONF_API_KEY): str}),
errors=errors,
)

View File

@@ -0,0 +1,7 @@
"""Constants for the TRMNL integration."""
import logging
DOMAIN = "trmnl"
LOGGER = logging.getLogger(__package__)

View File

@@ -0,0 +1,57 @@
"""Define an object to manage fetching TRMNL data."""
from __future__ import annotations
from datetime import timedelta
from trmnl import TRMNLClient
from trmnl.exceptions import TRMNLAuthenticationError, TRMNLError
from trmnl.models import Device
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, LOGGER
type TRMNLConfigEntry = ConfigEntry[TRMNLCoordinator]
class TRMNLCoordinator(DataUpdateCoordinator[dict[int, Device]]):
"""Class to manage fetching TRMNL data."""
config_entry: TRMNLConfigEntry
def __init__(self, hass: HomeAssistant, config_entry: TRMNLConfigEntry) -> None:
"""Initialize coordinator."""
super().__init__(
hass,
logger=LOGGER,
config_entry=config_entry,
name=DOMAIN,
update_interval=timedelta(hours=1),
)
self.client = TRMNLClient(
token=config_entry.data[CONF_API_KEY],
session=async_get_clientsession(hass),
)
async def _async_update_data(self) -> dict[int, Device]:
"""Fetch data from TRMNL."""
try:
devices = await self.client.get_devices()
except TRMNLAuthenticationError as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="authentication_error",
) from err
except TRMNLError as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_error",
translation_placeholders={"error": str(err)},
) from err
return {device.identifier: device for device in devices}

View File

@@ -0,0 +1,37 @@
"""Base class for TRMNL entities."""
from __future__ import annotations
from trmnl.models import Device
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .coordinator import TRMNLCoordinator
class TRMNLEntity(CoordinatorEntity[TRMNLCoordinator]):
"""Defines a base TRMNL entity."""
_attr_has_entity_name = True
def __init__(self, coordinator: TRMNLCoordinator, device_id: int) -> None:
"""Initialize TRMNL entity."""
super().__init__(coordinator)
self._device_id = device_id
device = self._device
self._attr_device_info = DeviceInfo(
connections={(CONNECTION_NETWORK_MAC, device.mac_address)},
name=device.name,
manufacturer="TRMNL",
)
@property
def _device(self) -> Device:
"""Return the device from coordinator data."""
return self.coordinator.data[self._device_id]
@property
def available(self) -> bool:
"""Return if the device is available."""
return super().available and self._device_id in self.coordinator.data

View File

@@ -0,0 +1,11 @@
{
"domain": "trmnl",
"name": "TRMNL",
"codeowners": ["@joostlek"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/trmnl",
"integration_type": "hub",
"iot_class": "cloud_polling",
"quality_scale": "bronze",
"requirements": ["trmnl==0.1.0"]
}

View File

@@ -0,0 +1,74 @@
rules:
# Bronze
action-setup:
status: exempt
comment: This integration does not provide additional actions.
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: This integration does not provide additional actions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: Entities of this integration do not explicitly subscribe to events.
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions: todo
config-entry-unloading: done
docs-configuration-parameters:
status: exempt
comment: There are no configuration parameters
docs-installation-parameters: todo
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: todo
test-coverage: todo
# Gold
devices: done
diagnostics: todo
discovery-update-info:
status: exempt
comment: Uses the cloud API
discovery:
status: exempt
comment: Can't be discovered
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: todo
docs-supported-functions: todo
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices: todo
entity-category: todo
entity-device-class: todo
entity-disabled-by-default: todo
entity-translations: done
exception-translations: todo
icon-translations: todo
reconfiguration-flow: todo
repair-issues:
status: exempt
comment: There are no repairable issues
stale-devices: todo
# Platinum
async-dependency: done
inject-websession: done
strict-typing: done

View File

@@ -0,0 +1,92 @@
"""Support for TRMNL sensors."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from trmnl.models import Device
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import (
PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
EntityCategory,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import TRMNLConfigEntry
from .coordinator import TRMNLCoordinator
from .entity import TRMNLEntity
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class TRMNLSensorEntityDescription(SensorEntityDescription):
"""Describes a TRMNL sensor entity."""
value_fn: Callable[[Device], int | float | None]
SENSOR_DESCRIPTIONS: tuple[TRMNLSensorEntityDescription, ...] = (
TRMNLSensorEntityDescription(
key="battery",
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda device: device.percent_charged,
),
TRMNLSensorEntityDescription(
key="rssi",
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda device: device.rssi,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: TRMNLConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up TRMNL sensor entities based on a config entry."""
coordinator = entry.runtime_data
async_add_entities(
TRMNLSensor(coordinator, device_id, description)
for device_id in coordinator.data
for description in SENSOR_DESCRIPTIONS
)
class TRMNLSensor(TRMNLEntity, SensorEntity):
"""Defines a TRMNL sensor."""
entity_description: TRMNLSensorEntityDescription
def __init__(
self,
coordinator: TRMNLCoordinator,
device_id: int,
description: TRMNLSensorEntityDescription,
) -> None:
"""Initialize TRMNL sensor."""
super().__init__(coordinator, device_id)
self.entity_description = description
self._attr_unique_id = f"{device_id}_{description.key}"
@property
def native_value(self) -> int | float | None:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self._device)

View File

@@ -0,0 +1,30 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"user": {
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]"
},
"data_description": {
"api_key": "The API key for your TRMNL account."
}
}
}
},
"exceptions": {
"authentication_error": {
"message": "Authentication failed. Please check your API key."
},
"update_error": {
"message": "An error occurred while communicating with TRMNL: {error}"
}
}
}

View File

@@ -743,6 +743,7 @@ FLOWS = {
"trane",
"transmission",
"triggercmd",
"trmnl",
"tuya",
"twentemilieu",
"twilio",

View File

@@ -7244,6 +7244,12 @@
"config_flow": true,
"iot_class": "cloud_polling"
},
"trmnl": {
"name": "TRMNL",
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_polling"
},
"tuya": {
"name": "Tuya",
"integration_type": "hub",

10
mypy.ini generated
View File

@@ -5458,6 +5458,16 @@ disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.trmnl.*]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.tts.*]
check_untyped_defs = true
disallow_incomplete_defs = true

3
requirements_all.txt generated
View File

@@ -3129,6 +3129,9 @@ transmission-rpc==7.0.3
# homeassistant.components.triggercmd
triggercmd==0.0.36
# homeassistant.components.trmnl
trmnl==0.1.0
# homeassistant.components.twinkly
ttls==1.8.3

View File

@@ -2632,6 +2632,9 @@ transmission-rpc==7.0.3
# homeassistant.components.triggercmd
triggercmd==0.0.36
# homeassistant.components.trmnl
trmnl==0.1.0
# homeassistant.components.twinkly
ttls==1.8.3

View File

@@ -0,0 +1,13 @@
"""Tests for the TRMNL integration."""
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
"""Set up the TRMNL integration for testing."""
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()

View File

@@ -0,0 +1,55 @@
"""Common fixtures for the TRMNL tests."""
from collections.abc import Generator
from unittest.mock import AsyncMock, patch
import pytest
from trmnl.models import DevicesResponse, UserResponse
from homeassistant.components.trmnl.const import DOMAIN
from homeassistant.const import CONF_API_KEY
from tests.common import MockConfigEntry, load_fixture
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock]:
"""Override async_setup_entry."""
with patch(
"homeassistant.components.trmnl.async_setup_entry", return_value=True
) as mock_setup_entry:
yield mock_setup_entry
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
"""Mock config entry."""
return MockConfigEntry(
domain=DOMAIN,
title="Test",
unique_id="30561",
data={CONF_API_KEY: "user_aaaaaaaaaa"},
)
@pytest.fixture
def mock_trmnl_client() -> Generator[AsyncMock]:
"""Mock TRMNL client."""
with (
patch(
"homeassistant.components.trmnl.coordinator.TRMNLClient",
autospec=True,
) as mock_client,
patch(
"homeassistant.components.trmnl.config_flow.TRMNLClient",
new=mock_client,
),
):
client = mock_client.return_value
client.get_me.return_value = UserResponse.from_json(
load_fixture("me.json", DOMAIN)
).data
client.get_devices.return_value = DevicesResponse.from_json(
load_fixture("devices.json", DOMAIN)
).data
yield client

View File

@@ -0,0 +1,17 @@
{
"data": [
{
"id": 42793,
"name": "Test TRMNL",
"friendly_id": "1RJXS4",
"mac_address": "B0:A6:04:AA:BB:CC",
"battery_voltage": 3.87,
"rssi": -64,
"sleep_mode_enabled": false,
"sleep_start_time": 1320,
"sleep_end_time": 480,
"percent_charged": 72.5,
"wifi_strength": 50
}
]
}

View File

@@ -0,0 +1,14 @@
{
"data": {
"id": 30561,
"name": "Test",
"email": "test@outlook.com",
"first_name": "test",
"last_name": "test",
"locale": "en",
"time_zone": "Amsterdam",
"time_zone_iana": "Europe/Amsterdam",
"utc_offset": 3600,
"api_key": "user_aaaaaaaaaa"
}
}

View File

@@ -0,0 +1,32 @@
# serializer version: 1
# name: test_device
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'config_entries_subentries': <ANY>,
'configuration_url': None,
'connections': set({
tuple(
'mac',
'b0:a6:04:aa:bb:cc',
),
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
}),
'labels': set({
}),
'manufacturer': 'TRMNL',
'model': None,
'model_id': None,
'name': 'Test TRMNL',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'sw_version': None,
'via_device_id': None,
})
# ---

View File

@@ -0,0 +1,109 @@
# serializer version: 1
# name: test_all_entities[sensor.test_trmnl_battery-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.test_trmnl_battery',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Battery',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.BATTERY: 'battery'>,
'original_icon': None,
'original_name': 'Battery',
'platform': 'trmnl',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '42793_battery',
'unit_of_measurement': '%',
})
# ---
# name: test_all_entities[sensor.test_trmnl_battery-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'battery',
'friendly_name': 'Test TRMNL Battery',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.test_trmnl_battery',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '72.5',
})
# ---
# name: test_all_entities[sensor.test_trmnl_signal_strength-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.test_trmnl_signal_strength',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Signal strength',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.SIGNAL_STRENGTH: 'signal_strength'>,
'original_icon': None,
'original_name': 'Signal strength',
'platform': 'trmnl',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '42793_rssi',
'unit_of_measurement': 'dBm',
})
# ---
# name: test_all_entities[sensor.test_trmnl_signal_strength-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'signal_strength',
'friendly_name': 'Test TRMNL Signal strength',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'dBm',
}),
'context': <ANY>,
'entity_id': 'sensor.test_trmnl_signal_strength',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '-64',
})
# ---

View File

@@ -0,0 +1,92 @@
"""Test the TRMNL config flow."""
from unittest.mock import AsyncMock
import pytest
from trmnl.exceptions import TRMNLAuthenticationError, TRMNLError
from homeassistant.components.trmnl.const import DOMAIN
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import CONF_API_KEY
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from tests.common import MockConfigEntry
@pytest.mark.usefixtures("mock_trmnl_client")
async def test_full_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None:
"""Test the full config flow."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_API_KEY: "user_aaaaaaaaaa"}
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "Test"
assert result["data"] == {CONF_API_KEY: "user_aaaaaaaaaa"}
assert result["result"].unique_id == "30561"
assert len(mock_setup_entry.mock_calls) == 1
@pytest.mark.parametrize(
("exception", "error"),
[
(TRMNLAuthenticationError, "invalid_auth"),
(TRMNLError, "cannot_connect"),
(Exception, "unknown"),
],
)
async def test_form_errors(
hass: HomeAssistant,
mock_trmnl_client: AsyncMock,
mock_setup_entry: AsyncMock,
exception: type[Exception],
error: str,
) -> None:
"""Test we handle form errors."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
mock_trmnl_client.get_me.side_effect = exception
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_API_KEY: "user_aaaaaaaaaa"}
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": error}
mock_trmnl_client.get_me.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_API_KEY: "user_aaaaaaaaaa"}
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert len(mock_setup_entry.mock_calls) == 1
@pytest.mark.usefixtures("mock_trmnl_client")
async def test_duplicate_entry(
hass: HomeAssistant, mock_config_entry: MockConfigEntry
) -> None:
"""Test we handle duplicate entries."""
mock_config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_API_KEY: "user_aaaaaaaaaa"}
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"

View File

@@ -0,0 +1,47 @@
"""Test the TRMNL initialization."""
from unittest.mock import AsyncMock
from syrupy.assertion import SnapshotAssertion
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from . import setup_integration
from tests.common import MockConfigEntry
async def test_load_unload_entry(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_trmnl_client: AsyncMock,
) -> None:
"""Test loading and unloading a config entry."""
await setup_integration(hass, mock_config_entry)
assert mock_config_entry.state is ConfigEntryState.LOADED
await hass.config_entries.async_unload(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
async def test_device(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
mock_config_entry: MockConfigEntry,
mock_trmnl_client: AsyncMock,
snapshot: SnapshotAssertion,
) -> None:
"""Test the TRMNL device."""
await setup_integration(hass, mock_config_entry)
device = device_registry.async_get_device(
connections={(CONNECTION_NETWORK_MAC, "B0:A6:04:AA:BB:CC")}
)
assert device
assert device == snapshot

View File

@@ -0,0 +1,29 @@
"""Tests for the TRMNL sensor."""
from unittest.mock import AsyncMock, patch
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import setup_integration
from tests.common import MockConfigEntry, snapshot_platform
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_all_entities(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
mock_trmnl_client: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test all sensor entities."""
with patch("homeassistant.components.trmnl.PLATFORMS", [Platform.SENSOR]):
await setup_integration(hass, mock_config_entry)
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)