mirror of
https://github.com/home-assistant/core.git
synced 2026-04-16 14:46:15 +02:00
Compare commits
2 Commits
fix-calend
...
python-3.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e937b09652 | ||
|
|
427faf4854 |
@@ -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'
|
||||
|
||||
2
.github/workflows/builder.yml
vendored
2
.github/workflows/builder.yml
vendored
@@ -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: {}
|
||||
|
||||
@@ -1 +1 @@
|
||||
3.14.2
|
||||
3.14.3
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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}:
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["twentemilieu"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["twentemilieu==3.0.0"]
|
||||
"requirements": ["twentemilieu==2.2.1"]
|
||||
}
|
||||
|
||||
@@ -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"]:
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
4
requirements_all.txt
generated
@@ -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
|
||||
|
||||
7
requirements_test_all.txt
generated
7
requirements_test_all.txt
generated
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user