Compare commits

..

2 Commits

Author SHA1 Message Date
Ariel Ebersberger
e937b09652 Merge branch 'dev' into python-3.14.3 2026-04-15 19:04:03 +02:00
Jan Čermák
427faf4854 Bump base image to 2026.02.0 with Python 3.14.3, use 3.14.3 in CI
This also bumps libcec used in the base image to 7.1.1, full changelog:
* https://github.com/home-assistant/docker/releases/tag/2026.02.0

Python changelog:
* https://docs.python.org/release/3.14.3/whatsnew/changelog.html
2026-04-10 10:23:28 +02:00
22 changed files with 269 additions and 207 deletions

View File

@@ -186,11 +186,15 @@ If `CHANGE_TYPE` IS "Breaking change" or "Deprecation", keep the `## Breaking ch
## Step 10: Push Branch and Create PR
Push the branch with upstream tracking, and create a PR against `home-assistant/core` with the generated title and body:
```bash
# Get branch name and GitHub username
BRANCH=$(git branch --show-current)
PUSH_REMOTE=$(git config "branch.$BRANCH.remote" 2>/dev/null || git remote | head -1)
GITHUB_USER=$(gh api user --jq .login 2>/dev/null || git remote get-url "$PUSH_REMOTE" | sed -E 's#.*[:/]([^/]+)/([^/]+)(\.git)?$#\1#')
# Create PR (gh pr create pushes the branch automatically)
gh pr create --repo home-assistant/core --base dev \
--head "$GITHUB_USER:$BRANCH" \
--draft \
--title "TITLE_HERE" \
--body "$(cat <<'EOF'

View File

@@ -14,7 +14,7 @@ env:
UV_HTTP_TIMEOUT: 60
UV_SYSTEM_PYTHON: "true"
# Base image version from https://github.com/home-assistant/docker
BASE_IMAGE_VERSION: "2026.01.0"
BASE_IMAGE_VERSION: "2026.02.0"
ARCHITECTURES: '["amd64", "aarch64"]'
permissions: {}

View File

@@ -1 +1 @@
3.14.2
3.14.3

View File

@@ -57,12 +57,9 @@ EVENT_DURATION = "duration"
# Fields for the list events service
LIST_EVENT_FIELDS = {
EVENT_UID,
"start",
"end",
EVENT_SUMMARY,
EVENT_DESCRIPTION,
EVENT_LOCATION,
EVENT_RECURRENCE_ID,
EVENT_RRULE,
}

View File

@@ -4,7 +4,6 @@ from __future__ import annotations
from typing import Any
from homeassistant.core import callback
from homeassistant.helpers.entity import Entity
from .const import DOMAIN, EVENT_HDMI_CEC_UNAVAILABLE
@@ -56,10 +55,9 @@ class CecEntity(Entity):
else:
self._attr_name = f"{self._device.type_name} {self._logical_address} ({self._device.osd_name})"
@callback
def _hdmi_cec_unavailable(self, callback_event):
self._attr_available = False
self.async_write_ha_state()
self.schedule_update_ha_state(False)
async def async_added_to_hass(self) -> None:
"""Register HDMI callbacks after initialization."""

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
import logging
from typing import Any
from pycec.commands import CecCommand, KeyPressCommand, KeyReleaseCommand
from pycec.const import (
@@ -30,6 +31,7 @@ from homeassistant.components.media_player import (
MediaPlayerEntity,
MediaPlayerEntityFeature,
MediaPlayerState,
MediaType,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -43,20 +45,20 @@ _LOGGER = logging.getLogger(__name__)
ENTITY_ID_FORMAT = MP_DOMAIN + ".{}"
async def async_setup_platform(
def setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Find and return HDMI devices as media players."""
"""Find and return HDMI devices as +switches."""
if discovery_info and ATTR_NEW in discovery_info:
_LOGGER.debug("Setting up HDMI devices %s", discovery_info[ATTR_NEW])
entities = []
for device in discovery_info[ATTR_NEW]:
hdmi_device = hass.data[DOMAIN][device]
entities.append(CecPlayerEntity(hdmi_device, hdmi_device.logical_address))
async_add_entities(entities, True)
add_entities(entities, True)
class CecPlayerEntity(CecEntity, MediaPlayerEntity):
@@ -77,61 +79,78 @@ class CecPlayerEntity(CecEntity, MediaPlayerEntity):
def send_playback(self, key):
"""Send playback status to CEC adapter."""
self._device.send_command(CecCommand(key, dst=self._logical_address))
self._device.async_send_command(CecCommand(key, dst=self._logical_address))
async def async_mute_volume(self, mute: bool) -> None:
def mute_volume(self, mute: bool) -> None:
"""Mute volume."""
self.send_keypress(KEY_MUTE_TOGGLE)
async def async_media_previous_track(self) -> None:
def media_previous_track(self) -> None:
"""Go to previous track."""
self.send_keypress(KEY_BACKWARD)
async def async_turn_on(self) -> None:
def turn_on(self) -> None:
"""Turn device on."""
self._device.turn_on()
self._attr_state = MediaPlayerState.ON
self.async_write_ha_state()
async def async_turn_off(self) -> None:
def clear_playlist(self) -> None:
"""Clear players playlist."""
raise NotImplementedError
def turn_off(self) -> None:
"""Turn device off."""
self._device.turn_off()
self._attr_state = MediaPlayerState.OFF
self.async_write_ha_state()
async def async_media_stop(self) -> None:
def media_stop(self) -> None:
"""Stop playback."""
self.send_keypress(KEY_STOP)
self._attr_state = MediaPlayerState.IDLE
self.async_write_ha_state()
async def async_media_next_track(self) -> None:
def play_media(
self, media_type: MediaType | str, media_id: str, **kwargs: Any
) -> None:
"""Not supported."""
raise NotImplementedError
def media_next_track(self) -> None:
"""Skip to next track."""
self.send_keypress(KEY_FORWARD)
async def async_media_pause(self) -> None:
def media_seek(self, position: float) -> None:
"""Not supported."""
raise NotImplementedError
def set_volume_level(self, volume: float) -> None:
"""Set volume level, range 0..1."""
raise NotImplementedError
def media_pause(self) -> None:
"""Pause playback."""
self.send_keypress(KEY_PAUSE)
self._attr_state = MediaPlayerState.PAUSED
self.async_write_ha_state()
async def async_media_play(self) -> None:
def select_source(self, source: str) -> None:
"""Not supported."""
raise NotImplementedError
def media_play(self) -> None:
"""Start playback."""
self.send_keypress(KEY_PLAY)
self._attr_state = MediaPlayerState.PLAYING
self.async_write_ha_state()
async def async_volume_up(self) -> None:
def volume_up(self) -> None:
"""Increase volume."""
_LOGGER.debug("%s: volume up", self._logical_address)
self.send_keypress(KEY_VOLUME_UP)
async def async_volume_down(self) -> None:
def volume_down(self) -> None:
"""Decrease volume."""
_LOGGER.debug("%s: volume down", self._logical_address)
self.send_keypress(KEY_VOLUME_DOWN)
async def async_update(self) -> None:
def update(self) -> None:
"""Update device status."""
device = self._device
if device.power_status in [POWER_OFF, 3]:

View File

@@ -20,10 +20,10 @@ _LOGGER = logging.getLogger(__name__)
ENTITY_ID_FORMAT = SWITCH_DOMAIN + ".{}"
async def async_setup_platform(
def setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Find and return HDMI devices as switches."""
@@ -33,7 +33,7 @@ async def async_setup_platform(
for device in discovery_info[ATTR_NEW]:
hdmi_device = hass.data[DOMAIN][device]
entities.append(CecSwitchEntity(hdmi_device, hdmi_device.logical_address))
async_add_entities(entities, True)
add_entities(entities, True)
class CecSwitchEntity(CecEntity, SwitchEntity):
@@ -44,19 +44,19 @@ class CecSwitchEntity(CecEntity, SwitchEntity):
CecEntity.__init__(self, device, logical)
self.entity_id = f"{SWITCH_DOMAIN}.hdmi_{hex(self._logical_address)[2:]}"
async def async_turn_on(self, **kwargs: Any) -> None:
def turn_on(self, **kwargs: Any) -> None:
"""Turn device on."""
self._device.turn_on()
self._attr_is_on = True
self.async_write_ha_state()
self.schedule_update_ha_state(force_refresh=False)
async def async_turn_off(self, **kwargs: Any) -> None:
def turn_off(self, **kwargs: Any) -> None:
"""Turn device off."""
self._device.turn_off()
self._attr_is_on = False
self.async_write_ha_state()
self.schedule_update_ha_state(force_refresh=False)
async def async_update(self) -> None:
def update(self) -> None:
"""Update device status."""
device = self._device
if device.power_status in {POWER_OFF, 3}:

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["twentemilieu"],
"quality_scale": "silver",
"requirements": ["twentemilieu==3.0.0"]
"requirements": ["twentemilieu==2.2.1"]
}

View File

@@ -26,20 +26,24 @@ from homeassistant.core import (
from homeassistant.helpers import config_validation as cv, discovery_flow
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.service_info.usb import UsbServiceInfo
from homeassistant.helpers.service_info.usb import UsbServiceInfo as _UsbServiceInfo
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import USBMatcher, async_get_usb
from homeassistant.util.hass_dict import HassKey
from .const import DOMAIN
from .models import SerialDevice, USBDevice
from .models import (
SerialDevice, # noqa: F401
USBDevice,
)
from .utils import (
async_scan_serial_ports,
scan_serial_ports,
usb_device_from_path,
scan_serial_ports, # noqa: F401
usb_device_from_path, # noqa: F401
usb_device_from_port, # noqa: F401
usb_device_matches_matcher,
usb_service_info_from_device,
usb_unique_id_from_service_info,
usb_unique_id_from_service_info, # noqa: F401
)
_LOGGER = logging.getLogger(__name__)
@@ -52,17 +56,9 @@ REQUEST_SCAN_COOLDOWN = 10 # 10 second cooldown
ADD_REMOVE_SCAN_COOLDOWN = 5 # 5 second cooldown to give devices a chance to register
__all__ = [
"SerialDevice",
"USBCallbackMatcher",
"USBDevice",
"async_register_port_event_callback",
"async_register_scan_request_callback",
"async_scan_serial_ports",
"scan_serial_ports",
"usb_device_from_path",
"usb_device_matches_matcher",
"usb_service_info_from_device",
"usb_unique_id_from_service_info",
]
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
@@ -362,7 +358,7 @@ class USBDiscovery:
for matcher in matched:
for flow in self.hass.config_entries.flow.async_progress_by_init_data_type(
UsbServiceInfo,
_UsbServiceInfo,
lambda flow_service_info: flow_service_info == service_info,
):
if matcher["domain"] != flow["handler"]:

View File

@@ -7,5 +7,5 @@
"integration_type": "system",
"iot_class": "local_push",
"quality_scale": "internal",
"requirements": ["aiousbwatcher==1.1.1", "serialx==1.2.2"]
"requirements": ["aiousbwatcher==1.1.1", "pyserial==3.5"]
}

View File

@@ -3,10 +3,12 @@
from __future__ import annotations
from collections.abc import Sequence
import dataclasses
import fnmatch
import os
from serialx import SerialPortInfo, list_serial_ports
from serial.tools.list_ports import comports
from serial.tools.list_ports_common import ListPortInfo
from homeassistant.core import HomeAssistant
from homeassistant.helpers.service_info.usb import UsbServiceInfo
@@ -15,8 +17,8 @@ from homeassistant.loader import USBMatcher
from .models import SerialDevice, USBDevice
def usb_device_from_port(port: SerialPortInfo) -> USBDevice:
"""Convert serialx SerialPortInfo to USBDevice."""
def usb_device_from_port(port: ListPortInfo) -> USBDevice:
"""Convert serial ListPortInfo to USBDevice."""
assert port.vid is not None
assert port.pid is not None
@@ -26,30 +28,53 @@ def usb_device_from_port(port: SerialPortInfo) -> USBDevice:
pid=f"{hex(port.pid)[2:]:0>4}".upper(),
serial_number=port.serial_number,
manufacturer=port.manufacturer,
description=port.product,
description=port.description,
)
def serial_device_from_port(port: SerialPortInfo) -> SerialDevice:
"""Convert serialx SerialPortInfo to SerialDevice."""
def serial_device_from_port(port: ListPortInfo) -> SerialDevice:
"""Convert serial ListPortInfo to SerialDevice."""
return SerialDevice(
device=port.device,
serial_number=port.serial_number,
manufacturer=port.manufacturer,
description=port.product,
description=port.description,
)
def usb_serial_device_from_port(port: SerialPortInfo) -> USBDevice | SerialDevice:
"""Convert serialx SerialPortInfo to USBDevice or SerialDevice."""
if port.vid is not None and port.pid is not None:
def usb_serial_device_from_port(port: ListPortInfo) -> USBDevice | SerialDevice:
"""Convert serial ListPortInfo to USBDevice or SerialDevice."""
if port.vid is not None or port.pid is not None:
assert port.vid is not None
assert port.pid is not None
return usb_device_from_port(port)
return serial_device_from_port(port)
def scan_serial_ports() -> Sequence[USBDevice | SerialDevice]:
"""Scan serial ports and return USB and other serial devices."""
return [usb_serial_device_from_port(port) for port in list_serial_ports()]
# Scan all symlinks first
by_id = "/dev/serial/by-id"
realpath_to_by_id: dict[str, str] = {}
if os.path.isdir(by_id):
for path in (entry.path for entry in os.scandir(by_id) if entry.is_symlink()):
realpath_to_by_id[os.path.realpath(path)] = path
serial_ports = []
for port in comports():
device = usb_serial_device_from_port(port)
device_path = realpath_to_by_id.get(port.device, port.device)
if device_path != port.device:
# Prefer the unique /dev/serial/by-id/ path if it exists
device = dataclasses.replace(device, device=device_path)
serial_ports.append(device)
return serial_ports
async def async_scan_serial_ports(

View File

@@ -57,13 +57,13 @@ PyJWT==2.10.1
pymicro-vad==1.0.1
PyNaCl==1.6.2
pyOpenSSL==26.0.0
pyserial==3.5
pyspeex-noise==1.0.2
python-slugify==8.0.4
PyTurboJPEG==1.8.0
PyYAML==6.0.3
requests==2.33.1
securetar==2026.4.1
serialx==1.2.2
SQLAlchemy==2.0.49
standard-aifc==3.13.0
standard-telnetlib==3.13.0

4
requirements_all.txt generated
View File

@@ -2469,6 +2469,7 @@ pysenz==1.0.2
pyserial-asyncio-fast==0.16
# homeassistant.components.acer_projector
# homeassistant.components.usb
pyserial==3.5
# homeassistant.components.sesame
@@ -2928,7 +2929,6 @@ sentence-stream==1.2.0
sentry-sdk==2.48.0
# homeassistant.components.homeassistant_hardware
# homeassistant.components.usb
# homeassistant.components.zha
serialx==1.2.2
@@ -3163,7 +3163,7 @@ tuya-device-handlers==0.0.17
tuya-device-sharing-sdk==0.2.8
# homeassistant.components.twentemilieu
twentemilieu==3.0.0
twentemilieu==2.2.1
# homeassistant.components.twilio
twilio==6.32.0

View File

@@ -2109,6 +2109,10 @@ pysensibo==1.2.1
# homeassistant.components.senz
pysenz==1.0.2
# homeassistant.components.acer_projector
# homeassistant.components.usb
pyserial==3.5
# homeassistant.components.seventeentrack
pyseventeentrack==1.1.3
@@ -2482,7 +2486,6 @@ sentence-stream==1.2.0
sentry-sdk==2.48.0
# homeassistant.components.homeassistant_hardware
# homeassistant.components.usb
# homeassistant.components.zha
serialx==1.2.2
@@ -2672,7 +2675,7 @@ tuya-device-handlers==0.0.17
tuya-device-sharing-sdk==0.2.8
# homeassistant.components.twentemilieu
twentemilieu==3.0.0
twentemilieu==2.2.1
# homeassistant.components.twilio
twilio==6.32.0

View File

@@ -196,9 +196,6 @@ def create_test_entities() -> list[MockCalendarEntity]:
summary="Future Event",
description="Future Description",
location="Future Location",
uid="calendar-event-uid-1",
rrule="FREQ=WEEKLY;COUNT=3",
recurrence_id="20260415",
)
],
unique_id="calendar_1_id",

View File

@@ -15,11 +15,8 @@
'description': 'Future Description',
'end': '2023-10-19T09:20:05-06:00',
'location': 'Future Location',
'recurrence_id': '20260415',
'rrule': 'FREQ=WEEKLY;COUNT=3',
'start': '2023-10-19T08:20:05-06:00',
'summary': 'Future Event',
'uid': 'calendar-event-uid-1',
}),
]),
}),

View File

@@ -435,9 +435,6 @@ async def test_create_event_service_invalid_params(
"summary": "Future Event",
"description": "Future Description",
"location": "Future Location",
"uid": "calendar-event-uid-1",
"rrule": "FREQ=WEEKLY;COUNT=3",
"recurrence_id": "20260415",
}
]
}
@@ -752,9 +749,6 @@ async def test_websocket_handle_subscribe_calendar_events(
events = msg["event"]["events"]
assert len(events) == 1
assert events[0]["summary"] == "Future Event"
assert events[0]["uid"] == "calendar-event-uid-1"
assert events[0]["rrule"] == "FREQ=WEEKLY;COUNT=3"
assert events[0]["recurrence_id"] == "20260415"
async def test_websocket_subscribe_updates_on_state_change(

View File

@@ -1,5 +1,6 @@
"""Tests for the HDMI-CEC media player platform."""
from collections.abc import Callable
from typing import Any
from pycec.const import (
@@ -57,6 +58,39 @@ from homeassistant.core import HomeAssistant
from . import MockHDMIDevice, assert_key_press_release
from .conftest import CecEntityCreator, HDMINetworkCreator
type AssertState = Callable[[str, str], None]
@pytest.fixture(
name="assert_state",
params=[
False,
pytest.param(
True,
marks=pytest.mark.xfail(
reason="""State isn't updated because the function is missing the
`schedule_update_ha_state` for a correct push entity. Would still
update once the data comes back from the device."""
),
),
],
ids=["skip_assert_state", "run_assert_state"],
)
def assert_state_fixture(request: pytest.FixtureRequest) -> AssertState:
"""Allow for skipping the assert state changes.
This is broken in this entity, but we still want to test that
the rest of the code works as expected.
"""
def _test_state(state: str, expected: str) -> None:
if request.param:
assert state == expected
else:
assert True
return _test_state
async def test_load_platform(
hass: HomeAssistant,
@@ -108,6 +142,7 @@ async def test_service_on(
hass: HomeAssistant,
create_hdmi_network: HDMINetworkCreator,
create_cec_entity: CecEntityCreator,
assert_state: AssertState,
) -> None:
"""Test that media_player triggers on `on` service."""
hdmi_network = await create_hdmi_network({"platform": "media_player"})
@@ -122,17 +157,19 @@ async def test_service_on(
{ATTR_ENTITY_ID: "media_player.hdmi_3"},
blocking=True,
)
await hass.async_block_till_done()
mock_hdmi_device.turn_on.assert_called_once_with()
state = hass.states.get("media_player.hdmi_3")
assert state.state == STATE_ON
assert_state(state.state, STATE_ON)
async def test_service_off(
hass: HomeAssistant,
create_hdmi_network: HDMINetworkCreator,
create_cec_entity: CecEntityCreator,
assert_state: AssertState,
) -> None:
"""Test that media_player triggers on `off` service."""
hdmi_network = await create_hdmi_network({"platform": "media_player"})
@@ -151,7 +188,7 @@ async def test_service_off(
mock_hdmi_device.turn_off.assert_called_once_with()
state = hass.states.get("media_player.hdmi_3")
assert state.state == STATE_OFF
assert_state(state.state, STATE_OFF)
@pytest.mark.parametrize(
@@ -280,6 +317,7 @@ async def test_volume_services(
data,
blocking=True,
)
await hass.async_block_till_done()
assert mock_hdmi_device.send_command.call_count == 2
assert_key_press_release(mock_hdmi_device.send_command, dst=3, key=key)
@@ -310,6 +348,7 @@ async def test_track_change_services(
{ATTR_ENTITY_ID: "media_player.hdmi_3"},
blocking=True,
)
await hass.async_block_till_done()
assert mock_hdmi_device.send_command.call_count == 2
assert_key_press_release(mock_hdmi_device.send_command, dst=3, key=key)
@@ -334,6 +373,7 @@ async def test_playback_services(
hass: HomeAssistant,
create_hdmi_network: HDMINetworkCreator,
create_cec_entity: CecEntityCreator,
assert_state: AssertState,
service: str,
key: int,
expected_state: str,
@@ -349,12 +389,13 @@ async def test_playback_services(
{ATTR_ENTITY_ID: "media_player.hdmi_3"},
blocking=True,
)
await hass.async_block_till_done()
assert mock_hdmi_device.send_command.call_count == 2
assert_key_press_release(mock_hdmi_device.send_command, dst=3, key=key)
state = hass.states.get("media_player.hdmi_3")
assert state.state == expected_state
assert_state(state.state, expected_state)
@pytest.mark.xfail(reason="PLAY feature isn't enabled")
@@ -362,6 +403,7 @@ async def test_play_pause_service(
hass: HomeAssistant,
create_hdmi_network: HDMINetworkCreator,
create_cec_entity: CecEntityCreator,
assert_state: AssertState,
) -> None:
"""Test play pause service."""
hdmi_network = await create_hdmi_network({"platform": "media_player"})
@@ -376,12 +418,13 @@ async def test_play_pause_service(
{ATTR_ENTITY_ID: "media_player.hdmi_3"},
blocking=True,
)
await hass.async_block_till_done()
assert mock_hdmi_device.send_command.call_count == 2
assert_key_press_release(mock_hdmi_device.send_command, dst=3, key=KEY_PAUSE)
state = hass.states.get("media_player.hdmi_3")
assert state.state == STATE_PAUSED
assert_state(state.state, STATE_PAUSED)
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
@@ -389,6 +432,7 @@ async def test_play_pause_service(
{ATTR_ENTITY_ID: "media_player.hdmi_3"},
blocking=True,
)
await hass.async_block_till_done()
assert mock_hdmi_device.send_command.call_count == 4
assert_key_press_release(mock_hdmi_device.send_command, 1, dst=3, key=KEY_PLAY)
@@ -483,6 +527,9 @@ async def test_starting_state(
assert state.state == expected_state
@pytest.mark.xfail(
reason="The code only sets the state to unavailable, doesn't set the `_attr_available` to false."
)
async def test_unavailable_status(
hass: HomeAssistant,
create_hdmi_network: HDMINetworkCreator,
@@ -494,7 +541,6 @@ async def test_unavailable_status(
await create_cec_entity(hdmi_network, mock_hdmi_device)
hass.bus.async_fire(EVENT_HDMI_CEC_UNAVAILABLE)
await hass.async_block_till_done()
state = hass.states.get("media_player.hdmi_3")
assert state.state == STATE_UNAVAILABLE

View File

@@ -2634,7 +2634,7 @@ async def help_test_reload_with_config(
"""Test reloading with supplied config."""
new_yaml_config_file = tmp_path / "configuration.yaml"
def _write_yaml_config() -> str:
def _write_yaml_config() -> None:
new_yaml_config = yaml.dump(config)
new_yaml_config_file.write_text(new_yaml_config)
assert new_yaml_config_file.read_text() == new_yaml_config

View File

@@ -536,6 +536,7 @@ async def test_loading_subentries(
async def test_loading_subentry_with_bad_component_schema(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
mqtt_config_subentries_data: tuple[dict[str, Any]],
device_registry: dr.DeviceRegistry,
caplog: pytest.LogCaptureFixture,
) -> None:
@@ -564,9 +565,10 @@ async def test_loading_subentry_with_bad_component_schema(
)
],
)
async def test_qos_on_mqtt_device_from_subentry(
async def test_qos_on_mqt_device_from_subentry(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
mqtt_config_subentries_data: tuple[dict[str, Any]],
device_registry: dr.DeviceRegistry,
) -> None:
"""Test QoS is set correctly on entities from MQTT device."""

View File

@@ -147,7 +147,6 @@
'end': '2023-12-31T21:00:00-08:00',
'start': '2023-12-31T16:00:00-08:00',
'summary': 'On peak: 0.22/kWh',
'uid': 'tariff_content_v2_Summer_ON_PEAK_2023-12-31T16:00:00-08:00',
}),
dict({
'description': '''
@@ -158,7 +157,6 @@
'end': '2024-01-01T16:00:00-08:00',
'start': '2023-12-31T21:00:00-08:00',
'summary': 'Off peak: 0.20/kWh',
'uid': 'tariff_content_v2_Summer_OFF_PEAK_2023-12-31T21:00:00-08:00',
}),
dict({
'description': '''
@@ -169,7 +167,6 @@
'end': '2024-01-01T21:00:00-08:00',
'start': '2024-01-01T16:00:00-08:00',
'summary': 'On peak: 0.22/kWh',
'uid': 'tariff_content_v2_Summer_ON_PEAK_2024-01-01T16:00:00-08:00',
}),
dict({
'description': '''
@@ -180,7 +177,6 @@
'end': '2024-01-02T16:00:00-08:00',
'start': '2024-01-01T21:00:00-08:00',
'summary': 'Off peak: 0.20/kWh',
'uid': 'tariff_content_v2_Summer_OFF_PEAK_2024-01-01T21:00:00-08:00',
}),
dict({
'description': '''
@@ -191,7 +187,6 @@
'end': '2024-01-02T21:00:00-08:00',
'start': '2024-01-02T16:00:00-08:00',
'summary': 'On peak: 0.22/kWh',
'uid': 'tariff_content_v2_Summer_ON_PEAK_2024-01-02T16:00:00-08:00',
}),
dict({
'description': '''
@@ -202,7 +197,6 @@
'end': '2024-01-03T16:00:00-08:00',
'start': '2024-01-02T21:00:00-08:00',
'summary': 'Off peak: 0.20/kWh',
'uid': 'tariff_content_v2_Summer_OFF_PEAK_2024-01-02T21:00:00-08:00',
}),
dict({
'description': '''
@@ -213,7 +207,6 @@
'end': '2024-01-03T21:00:00-08:00',
'start': '2024-01-03T16:00:00-08:00',
'summary': 'On peak: 0.22/kWh',
'uid': 'tariff_content_v2_Summer_ON_PEAK_2024-01-03T16:00:00-08:00',
}),
dict({
'description': '''
@@ -224,7 +217,6 @@
'end': '2024-01-04T16:00:00-08:00',
'start': '2024-01-03T21:00:00-08:00',
'summary': 'Off peak: 0.20/kWh',
'uid': 'tariff_content_v2_Summer_OFF_PEAK_2024-01-03T21:00:00-08:00',
}),
dict({
'description': '''
@@ -235,7 +227,6 @@
'end': '2024-01-04T21:00:00-08:00',
'start': '2024-01-04T16:00:00-08:00',
'summary': 'On peak: 0.22/kWh',
'uid': 'tariff_content_v2_Summer_ON_PEAK_2024-01-04T16:00:00-08:00',
}),
dict({
'description': '''
@@ -246,7 +237,6 @@
'end': '2024-01-05T16:00:00-08:00',
'start': '2024-01-04T21:00:00-08:00',
'summary': 'Off peak: 0.20/kWh',
'uid': 'tariff_content_v2_Summer_OFF_PEAK_2024-01-04T21:00:00-08:00',
}),
dict({
'description': '''
@@ -257,7 +247,6 @@
'end': '2024-01-05T21:00:00-08:00',
'start': '2024-01-05T16:00:00-08:00',
'summary': 'On peak: 0.22/kWh',
'uid': 'tariff_content_v2_Summer_ON_PEAK_2024-01-05T16:00:00-08:00',
}),
dict({
'description': '''
@@ -268,7 +257,6 @@
'end': '2024-01-06T16:00:00-08:00',
'start': '2024-01-05T21:00:00-08:00',
'summary': 'Off peak: 0.20/kWh',
'uid': 'tariff_content_v2_Summer_OFF_PEAK_2024-01-05T21:00:00-08:00',
}),
]),
}),
@@ -302,7 +290,6 @@
'end': '2023-12-31T21:00:00-08:00',
'start': '2023-12-31T16:00:00-08:00',
'summary': 'On peak: 0.16/kWh',
'uid': 'tariff_content_v2_sell_tariff_Summer_ON_PEAK_2023-12-31T16:00:00-08:00',
}),
dict({
'description': '''
@@ -313,7 +300,6 @@
'end': '2024-01-01T16:00:00-08:00',
'start': '2023-12-31T21:00:00-08:00',
'summary': 'Off peak: 0.08/kWh',
'uid': 'tariff_content_v2_sell_tariff_Summer_OFF_PEAK_2023-12-31T21:00:00-08:00',
}),
dict({
'description': '''
@@ -324,7 +310,6 @@
'end': '2024-01-01T21:00:00-08:00',
'start': '2024-01-01T16:00:00-08:00',
'summary': 'On peak: 0.16/kWh',
'uid': 'tariff_content_v2_sell_tariff_Summer_ON_PEAK_2024-01-01T16:00:00-08:00',
}),
dict({
'description': '''
@@ -335,7 +320,6 @@
'end': '2024-01-02T16:00:00-08:00',
'start': '2024-01-01T21:00:00-08:00',
'summary': 'Off peak: 0.08/kWh',
'uid': 'tariff_content_v2_sell_tariff_Summer_OFF_PEAK_2024-01-01T21:00:00-08:00',
}),
dict({
'description': '''
@@ -346,7 +330,6 @@
'end': '2024-01-02T21:00:00-08:00',
'start': '2024-01-02T16:00:00-08:00',
'summary': 'On peak: 0.16/kWh',
'uid': 'tariff_content_v2_sell_tariff_Summer_ON_PEAK_2024-01-02T16:00:00-08:00',
}),
dict({
'description': '''
@@ -357,7 +340,6 @@
'end': '2024-01-03T16:00:00-08:00',
'start': '2024-01-02T21:00:00-08:00',
'summary': 'Off peak: 0.08/kWh',
'uid': 'tariff_content_v2_sell_tariff_Summer_OFF_PEAK_2024-01-02T21:00:00-08:00',
}),
dict({
'description': '''
@@ -368,7 +350,6 @@
'end': '2024-01-03T21:00:00-08:00',
'start': '2024-01-03T16:00:00-08:00',
'summary': 'On peak: 0.16/kWh',
'uid': 'tariff_content_v2_sell_tariff_Summer_ON_PEAK_2024-01-03T16:00:00-08:00',
}),
dict({
'description': '''
@@ -379,7 +360,6 @@
'end': '2024-01-04T16:00:00-08:00',
'start': '2024-01-03T21:00:00-08:00',
'summary': 'Off peak: 0.08/kWh',
'uid': 'tariff_content_v2_sell_tariff_Summer_OFF_PEAK_2024-01-03T21:00:00-08:00',
}),
dict({
'description': '''
@@ -390,7 +370,6 @@
'end': '2024-01-04T21:00:00-08:00',
'start': '2024-01-04T16:00:00-08:00',
'summary': 'On peak: 0.16/kWh',
'uid': 'tariff_content_v2_sell_tariff_Summer_ON_PEAK_2024-01-04T16:00:00-08:00',
}),
dict({
'description': '''
@@ -401,7 +380,6 @@
'end': '2024-01-05T16:00:00-08:00',
'start': '2024-01-04T21:00:00-08:00',
'summary': 'Off peak: 0.08/kWh',
'uid': 'tariff_content_v2_sell_tariff_Summer_OFF_PEAK_2024-01-04T21:00:00-08:00',
}),
dict({
'description': '''
@@ -412,7 +390,6 @@
'end': '2024-01-05T21:00:00-08:00',
'start': '2024-01-05T16:00:00-08:00',
'summary': 'On peak: 0.16/kWh',
'uid': 'tariff_content_v2_sell_tariff_Summer_ON_PEAK_2024-01-05T16:00:00-08:00',
}),
dict({
'description': '''
@@ -423,7 +400,6 @@
'end': '2024-01-06T16:00:00-08:00',
'start': '2024-01-05T21:00:00-08:00',
'summary': 'Off peak: 0.08/kWh',
'uid': 'tariff_content_v2_sell_tariff_Summer_OFF_PEAK_2024-01-05T21:00:00-08:00',
}),
]),
}),
@@ -457,7 +433,6 @@
'end': '2023-12-31T21:00:00-08:00',
'start': '2023-12-31T16:00:00-08:00',
'summary': 'On peak: 0.22/kWh',
'uid': 'tariff_content_v2_Summer_ON_PEAK_2023-12-31T16:00:00-08:00',
}),
dict({
'description': '''
@@ -468,7 +443,6 @@
'end': '2024-01-01T16:00:00-08:00',
'start': '2023-12-31T21:00:00-08:00',
'summary': 'Off peak: 0.20/kWh',
'uid': 'tariff_content_v2_Summer_OFF_PEAK_2023-12-31T21:00:00-08:00',
}),
dict({
'description': '''
@@ -479,7 +453,6 @@
'end': '2024-01-01T21:00:00-08:00',
'start': '2024-01-01T16:00:00-08:00',
'summary': 'On peak: 0.22/kWh',
'uid': 'tariff_content_v2_Summer_ON_PEAK_2024-01-01T16:00:00-08:00',
}),
dict({
'description': '''
@@ -490,7 +463,6 @@
'end': '2024-01-02T16:00:00-08:00',
'start': '2024-01-01T21:00:00-08:00',
'summary': 'Off peak: 0.20/kWh',
'uid': 'tariff_content_v2_Summer_OFF_PEAK_2024-01-01T21:00:00-08:00',
}),
dict({
'description': '''
@@ -501,7 +473,6 @@
'end': '2024-01-02T21:00:00-08:00',
'start': '2024-01-02T16:00:00-08:00',
'summary': 'On peak: 0.22/kWh',
'uid': 'tariff_content_v2_Summer_ON_PEAK_2024-01-02T16:00:00-08:00',
}),
dict({
'description': '''
@@ -512,7 +483,6 @@
'end': '2024-01-03T16:00:00-08:00',
'start': '2024-01-02T21:00:00-08:00',
'summary': 'Off peak: 0.20/kWh',
'uid': 'tariff_content_v2_Summer_OFF_PEAK_2024-01-02T21:00:00-08:00',
}),
dict({
'description': '''
@@ -523,7 +493,6 @@
'end': '2024-01-03T21:00:00-08:00',
'start': '2024-01-03T16:00:00-08:00',
'summary': 'On peak: 0.22/kWh',
'uid': 'tariff_content_v2_Summer_ON_PEAK_2024-01-03T16:00:00-08:00',
}),
dict({
'description': '''
@@ -534,7 +503,6 @@
'end': '2024-01-04T16:00:00-08:00',
'start': '2024-01-03T21:00:00-08:00',
'summary': 'Off peak: 0.20/kWh',
'uid': 'tariff_content_v2_Summer_OFF_PEAK_2024-01-03T21:00:00-08:00',
}),
dict({
'description': '''
@@ -545,7 +513,6 @@
'end': '2024-01-04T21:00:00-08:00',
'start': '2024-01-04T16:00:00-08:00',
'summary': 'On peak: 0.22/kWh',
'uid': 'tariff_content_v2_Summer_ON_PEAK_2024-01-04T16:00:00-08:00',
}),
dict({
'description': '''
@@ -556,7 +523,6 @@
'end': '2024-01-05T16:00:00-08:00',
'start': '2024-01-04T21:00:00-08:00',
'summary': 'Off peak: 0.20/kWh',
'uid': 'tariff_content_v2_Summer_OFF_PEAK_2024-01-04T21:00:00-08:00',
}),
dict({
'description': '''
@@ -567,7 +533,6 @@
'end': '2024-01-05T21:00:00-08:00',
'start': '2024-01-05T16:00:00-08:00',
'summary': 'On peak: 0.22/kWh',
'uid': 'tariff_content_v2_Summer_ON_PEAK_2024-01-05T16:00:00-08:00',
}),
dict({
'description': '''
@@ -578,7 +543,6 @@
'end': '2024-01-06T16:00:00-08:00',
'start': '2024-01-05T21:00:00-08:00',
'summary': 'Off peak: 0.20/kWh',
'uid': 'tariff_content_v2_Summer_OFF_PEAK_2024-01-05T21:00:00-08:00',
}),
]),
}),
@@ -612,7 +576,6 @@
'end': '2023-12-31T21:00:00-08:00',
'start': '2023-12-31T16:00:00-08:00',
'summary': 'On peak: 0.16/kWh',
'uid': 'tariff_content_v2_sell_tariff_Summer_ON_PEAK_2023-12-31T16:00:00-08:00',
}),
dict({
'description': '''
@@ -623,7 +586,6 @@
'end': '2024-01-01T16:00:00-08:00',
'start': '2023-12-31T21:00:00-08:00',
'summary': 'Off peak: 0.08/kWh',
'uid': 'tariff_content_v2_sell_tariff_Summer_OFF_PEAK_2023-12-31T21:00:00-08:00',
}),
dict({
'description': '''
@@ -634,7 +596,6 @@
'end': '2024-01-01T21:00:00-08:00',
'start': '2024-01-01T16:00:00-08:00',
'summary': 'On peak: 0.16/kWh',
'uid': 'tariff_content_v2_sell_tariff_Summer_ON_PEAK_2024-01-01T16:00:00-08:00',
}),
dict({
'description': '''
@@ -645,7 +606,6 @@
'end': '2024-01-02T16:00:00-08:00',
'start': '2024-01-01T21:00:00-08:00',
'summary': 'Off peak: 0.08/kWh',
'uid': 'tariff_content_v2_sell_tariff_Summer_OFF_PEAK_2024-01-01T21:00:00-08:00',
}),
dict({
'description': '''
@@ -656,7 +616,6 @@
'end': '2024-01-02T21:00:00-08:00',
'start': '2024-01-02T16:00:00-08:00',
'summary': 'On peak: 0.16/kWh',
'uid': 'tariff_content_v2_sell_tariff_Summer_ON_PEAK_2024-01-02T16:00:00-08:00',
}),
dict({
'description': '''
@@ -667,7 +626,6 @@
'end': '2024-01-03T16:00:00-08:00',
'start': '2024-01-02T21:00:00-08:00',
'summary': 'Off peak: 0.08/kWh',
'uid': 'tariff_content_v2_sell_tariff_Summer_OFF_PEAK_2024-01-02T21:00:00-08:00',
}),
dict({
'description': '''
@@ -678,7 +636,6 @@
'end': '2024-01-03T21:00:00-08:00',
'start': '2024-01-03T16:00:00-08:00',
'summary': 'On peak: 0.16/kWh',
'uid': 'tariff_content_v2_sell_tariff_Summer_ON_PEAK_2024-01-03T16:00:00-08:00',
}),
dict({
'description': '''
@@ -689,7 +646,6 @@
'end': '2024-01-04T16:00:00-08:00',
'start': '2024-01-03T21:00:00-08:00',
'summary': 'Off peak: 0.08/kWh',
'uid': 'tariff_content_v2_sell_tariff_Summer_OFF_PEAK_2024-01-03T21:00:00-08:00',
}),
dict({
'description': '''
@@ -700,7 +656,6 @@
'end': '2024-01-04T21:00:00-08:00',
'start': '2024-01-04T16:00:00-08:00',
'summary': 'On peak: 0.16/kWh',
'uid': 'tariff_content_v2_sell_tariff_Summer_ON_PEAK_2024-01-04T16:00:00-08:00',
}),
dict({
'description': '''
@@ -711,7 +666,6 @@
'end': '2024-01-05T16:00:00-08:00',
'start': '2024-01-04T21:00:00-08:00',
'summary': 'Off peak: 0.08/kWh',
'uid': 'tariff_content_v2_sell_tariff_Summer_OFF_PEAK_2024-01-04T21:00:00-08:00',
}),
dict({
'description': '''
@@ -722,7 +676,6 @@
'end': '2024-01-05T21:00:00-08:00',
'start': '2024-01-05T16:00:00-08:00',
'summary': 'On peak: 0.16/kWh',
'uid': 'tariff_content_v2_sell_tariff_Summer_ON_PEAK_2024-01-05T16:00:00-08:00',
}),
dict({
'description': '''
@@ -733,7 +686,6 @@
'end': '2024-01-06T16:00:00-08:00',
'start': '2024-01-05T21:00:00-08:00',
'summary': 'Off peak: 0.08/kWh',
'uid': 'tariff_content_v2_sell_tariff_Summer_OFF_PEAK_2024-01-05T21:00:00-08:00',
}),
]),
}),
@@ -767,7 +719,6 @@
'end': '2023-12-31T21:00:00-08:00',
'start': '2023-12-31T16:00:00-08:00',
'summary': 'On peak: 0.22/kWh',
'uid': 'tariff_content_v2_Summer_ON_PEAK_2023-12-31T16:00:00-08:00',
}),
dict({
'description': '''
@@ -778,7 +729,6 @@
'end': '2024-01-01T16:00:00-08:00',
'start': '2023-12-31T21:00:00-08:00',
'summary': 'Off peak: 0.20/kWh',
'uid': 'tariff_content_v2_Summer_OFF_PEAK_2023-12-31T21:00:00-08:00',
}),
dict({
'description': '''
@@ -789,7 +739,6 @@
'end': '2024-01-01T21:00:00-08:00',
'start': '2024-01-01T16:00:00-08:00',
'summary': 'On peak: 0.22/kWh',
'uid': 'tariff_content_v2_Summer_ON_PEAK_2024-01-01T16:00:00-08:00',
}),
dict({
'description': '''
@@ -800,7 +749,6 @@
'end': '2024-01-02T16:00:00-08:00',
'start': '2024-01-01T21:00:00-08:00',
'summary': 'Off peak: 0.20/kWh',
'uid': 'tariff_content_v2_Summer_OFF_PEAK_2024-01-01T21:00:00-08:00',
}),
dict({
'description': '''
@@ -811,7 +759,6 @@
'end': '2024-01-02T21:00:00-08:00',
'start': '2024-01-02T16:00:00-08:00',
'summary': 'On peak: 0.22/kWh',
'uid': 'tariff_content_v2_Summer_ON_PEAK_2024-01-02T16:00:00-08:00',
}),
dict({
'description': '''
@@ -822,7 +769,6 @@
'end': '2024-01-03T16:00:00-08:00',
'start': '2024-01-02T21:00:00-08:00',
'summary': 'Off peak: 0.20/kWh',
'uid': 'tariff_content_v2_Summer_OFF_PEAK_2024-01-02T21:00:00-08:00',
}),
dict({
'description': '''
@@ -833,7 +779,6 @@
'end': '2024-01-03T21:00:00-08:00',
'start': '2024-01-03T16:00:00-08:00',
'summary': 'On peak: 0.22/kWh',
'uid': 'tariff_content_v2_Summer_ON_PEAK_2024-01-03T16:00:00-08:00',
}),
dict({
'description': '''
@@ -844,7 +789,6 @@
'end': '2024-01-04T16:00:00-08:00',
'start': '2024-01-03T21:00:00-08:00',
'summary': 'Off peak: 0.20/kWh',
'uid': 'tariff_content_v2_Summer_OFF_PEAK_2024-01-03T21:00:00-08:00',
}),
dict({
'description': '''
@@ -855,7 +799,6 @@
'end': '2024-01-04T21:00:00-08:00',
'start': '2024-01-04T16:00:00-08:00',
'summary': 'On peak: 0.22/kWh',
'uid': 'tariff_content_v2_Summer_ON_PEAK_2024-01-04T16:00:00-08:00',
}),
dict({
'description': '''
@@ -866,7 +809,6 @@
'end': '2024-01-05T16:00:00-08:00',
'start': '2024-01-04T21:00:00-08:00',
'summary': 'Off peak: 0.20/kWh',
'uid': 'tariff_content_v2_Summer_OFF_PEAK_2024-01-04T21:00:00-08:00',
}),
dict({
'description': '''
@@ -877,7 +819,6 @@
'end': '2024-01-05T21:00:00-08:00',
'start': '2024-01-05T16:00:00-08:00',
'summary': 'On peak: 0.22/kWh',
'uid': 'tariff_content_v2_Summer_ON_PEAK_2024-01-05T16:00:00-08:00',
}),
dict({
'description': '''
@@ -888,7 +829,6 @@
'end': '2024-01-06T16:00:00-08:00',
'start': '2024-01-05T21:00:00-08:00',
'summary': 'Off peak: 0.20/kWh',
'uid': 'tariff_content_v2_Summer_OFF_PEAK_2024-01-05T21:00:00-08:00',
}),
]),
}),
@@ -922,7 +862,6 @@
'end': '2023-12-31T21:00:00-08:00',
'start': '2023-12-31T16:00:00-08:00',
'summary': 'On peak: 0.16/kWh',
'uid': 'tariff_content_v2_sell_tariff_Summer_ON_PEAK_2023-12-31T16:00:00-08:00',
}),
dict({
'description': '''
@@ -933,7 +872,6 @@
'end': '2024-01-01T16:00:00-08:00',
'start': '2023-12-31T21:00:00-08:00',
'summary': 'Off peak: 0.08/kWh',
'uid': 'tariff_content_v2_sell_tariff_Summer_OFF_PEAK_2023-12-31T21:00:00-08:00',
}),
dict({
'description': '''
@@ -944,7 +882,6 @@
'end': '2024-01-01T21:00:00-08:00',
'start': '2024-01-01T16:00:00-08:00',
'summary': 'On peak: 0.16/kWh',
'uid': 'tariff_content_v2_sell_tariff_Summer_ON_PEAK_2024-01-01T16:00:00-08:00',
}),
dict({
'description': '''
@@ -955,7 +892,6 @@
'end': '2024-01-02T16:00:00-08:00',
'start': '2024-01-01T21:00:00-08:00',
'summary': 'Off peak: 0.08/kWh',
'uid': 'tariff_content_v2_sell_tariff_Summer_OFF_PEAK_2024-01-01T21:00:00-08:00',
}),
dict({
'description': '''
@@ -966,7 +902,6 @@
'end': '2024-01-02T21:00:00-08:00',
'start': '2024-01-02T16:00:00-08:00',
'summary': 'On peak: 0.16/kWh',
'uid': 'tariff_content_v2_sell_tariff_Summer_ON_PEAK_2024-01-02T16:00:00-08:00',
}),
dict({
'description': '''
@@ -977,7 +912,6 @@
'end': '2024-01-03T16:00:00-08:00',
'start': '2024-01-02T21:00:00-08:00',
'summary': 'Off peak: 0.08/kWh',
'uid': 'tariff_content_v2_sell_tariff_Summer_OFF_PEAK_2024-01-02T21:00:00-08:00',
}),
dict({
'description': '''
@@ -988,7 +922,6 @@
'end': '2024-01-03T21:00:00-08:00',
'start': '2024-01-03T16:00:00-08:00',
'summary': 'On peak: 0.16/kWh',
'uid': 'tariff_content_v2_sell_tariff_Summer_ON_PEAK_2024-01-03T16:00:00-08:00',
}),
dict({
'description': '''
@@ -999,7 +932,6 @@
'end': '2024-01-04T16:00:00-08:00',
'start': '2024-01-03T21:00:00-08:00',
'summary': 'Off peak: 0.08/kWh',
'uid': 'tariff_content_v2_sell_tariff_Summer_OFF_PEAK_2024-01-03T21:00:00-08:00',
}),
dict({
'description': '''
@@ -1010,7 +942,6 @@
'end': '2024-01-04T21:00:00-08:00',
'start': '2024-01-04T16:00:00-08:00',
'summary': 'On peak: 0.16/kWh',
'uid': 'tariff_content_v2_sell_tariff_Summer_ON_PEAK_2024-01-04T16:00:00-08:00',
}),
dict({
'description': '''
@@ -1021,7 +952,6 @@
'end': '2024-01-05T16:00:00-08:00',
'start': '2024-01-04T21:00:00-08:00',
'summary': 'Off peak: 0.08/kWh',
'uid': 'tariff_content_v2_sell_tariff_Summer_OFF_PEAK_2024-01-04T21:00:00-08:00',
}),
dict({
'description': '''
@@ -1032,7 +962,6 @@
'end': '2024-01-05T21:00:00-08:00',
'start': '2024-01-05T16:00:00-08:00',
'summary': 'On peak: 0.16/kWh',
'uid': 'tariff_content_v2_sell_tariff_Summer_ON_PEAK_2024-01-05T16:00:00-08:00',
}),
dict({
'description': '''
@@ -1043,7 +972,6 @@
'end': '2024-01-06T16:00:00-08:00',
'start': '2024-01-05T21:00:00-08:00',
'summary': 'Off peak: 0.08/kWh',
'uid': 'tariff_content_v2_sell_tariff_Summer_OFF_PEAK_2024-01-05T21:00:00-08:00',
}),
]),
}),

View File

@@ -7,7 +7,6 @@ import os
from unittest.mock import MagicMock, Mock, call, patch, sentinel
import pytest
from serialx import SerialPortInfo
from homeassistant import config_entries
from homeassistant.components import usb
@@ -1297,55 +1296,112 @@ async def test_register_port_event_callback_failure(
assert "Failure 2" in caplog.text
async def test_async_scan_serial_ports(hass: HomeAssistant) -> None:
"""Test async_scan_serial_ports parsing."""
with patch(
"homeassistant.components.usb.utils.list_serial_ports",
return_value=[
SerialPortInfo(
device="/dev/ttyAMA1",
resolved_device="/dev/ttyAMA1",
vid=None,
pid=None,
serial_number=None,
manufacturer=None,
product=None,
bcd_device=None,
interface_description=None,
interface_num=None,
),
SerialPortInfo(
device="/dev/serial/by-id/usb-Nabu_Casa_ZBT-2_10B41DE589FC-if00",
resolved_device="/dev/ttyACM0",
vid=12346,
pid=16385,
serial_number="10B41DE589FC",
manufacturer="Nabu Casa",
product="ZBT-2",
bcd_device=257,
interface_description="Nabu Casa ZBT-2",
interface_num=0,
),
],
async def test_async_scan_serial_ports_with_unique_symlinks(
hass: HomeAssistant,
) -> None:
"""Test async_scan_serial_ports returns devices with unique /dev/serial/by-id paths."""
entry1 = MagicMock(spec_set=os.DirEntry)
entry1.is_symlink.return_value = True
entry1.path = "/dev/serial/by-id/usb-device1"
entry2 = MagicMock(spec_set=os.DirEntry)
entry2.is_symlink.return_value = True
entry2.path = "/dev/serial/by-id/usb-device2"
mock_port1 = MagicMock()
mock_port1.device = "/dev/ttyUSB0"
mock_port1.vid = 0x1234
mock_port1.pid = 0x5678
mock_port1.serial_number = "ABC123"
mock_port1.manufacturer = "Test Manufacturer"
mock_port1.description = "Test Device"
mock_port2 = MagicMock()
mock_port2.device = "/dev/ttyUSB1"
mock_port2.vid = 0xABCD
mock_port2.pid = 0xEF01
mock_port2.serial_number = "XYZ789"
mock_port2.manufacturer = "Another Manufacturer"
mock_port2.description = "Another Device"
def mock_realpath(path: str) -> str:
realpath_map = {
"/dev/serial/by-id/usb-device1": "/dev/ttyUSB0",
"/dev/serial/by-id/usb-device2": "/dev/ttyUSB1",
}
return realpath_map.get(path, path)
with (
patch("os.path.isdir", return_value=True),
patch("os.scandir", return_value=[entry1, entry2]),
patch("os.path.realpath", side_effect=mock_realpath),
patch(
"homeassistant.components.usb.utils.comports",
return_value=[mock_port1, mock_port2],
),
):
devices = await async_scan_serial_ports(hass)
assert devices == [
SerialDevice(
device="/dev/ttyAMA1",
serial_number=None,
manufacturer=None,
description=None,
assert len(devices) == 2
assert devices[0].device == "/dev/serial/by-id/usb-device1"
assert devices[0].vid == "1234"
assert devices[1].device == "/dev/serial/by-id/usb-device2"
assert devices[1].vid == "ABCD"
async def test_async_scan_serial_ports_without_unique_symlinks(
hass: HomeAssistant,
) -> None:
"""Test async_scan_serial_ports returns devices with original paths when no symlinks exist."""
mock_port = MagicMock()
mock_port.device = "/dev/ttyUSB0"
mock_port.vid = 0x1234
mock_port.pid = 0x5678
mock_port.serial_number = "ABC123"
mock_port.manufacturer = "Test Manufacturer"
mock_port.description = "Test Device"
with (
patch("os.path.isdir", return_value=False),
patch("os.path.realpath", side_effect=lambda x: x),
patch(
"homeassistant.components.usb.utils.comports",
return_value=[mock_port],
),
USBDevice(
device="/dev/serial/by-id/usb-Nabu_Casa_ZBT-2_10B41DE589FC-if00",
vid="303A",
pid="4001",
serial_number="10B41DE589FC",
manufacturer="Nabu Casa",
description="ZBT-2",
):
devices = await async_scan_serial_ports(hass)
assert len(devices) == 1
assert devices[0].device == "/dev/ttyUSB0"
assert devices[0].vid == "1234"
async def test_async_scan_serial_ports_no_vid_pid(hass: HomeAssistant) -> None:
"""Test async_scan_serial_ports returns devices without VID:PID."""
mock_port = MagicMock()
mock_port.device = "/dev/ttyAMA1"
mock_port.vid = None
mock_port.pid = None
mock_port.serial_number = None
mock_port.manufacturer = None
mock_port.description = None
with (
patch("os.path.isdir", return_value=False),
patch("os.path.realpath", side_effect=lambda x: x),
patch(
"homeassistant.components.usb.utils.comports",
return_value=[mock_port],
),
]
):
devices = await async_scan_serial_ports(hass)
assert len(devices) == 1
assert isinstance(devices[0], SerialDevice)
assert devices[0].device == "/dev/ttyAMA1"
assert devices[0].serial_number is None
assert devices[0].manufacturer is None
assert devices[0].description is None
def test_usb_device_from_path_finds_by_symlink() -> None: