Bump pysnooz to 0.10.0 (#182225)

This commit is contained in:
Jamie Magee
2026-09-18 13:54:26 +00:00
committed by GitHub
parent d7be305892
commit 32413f4a70
8 changed files with 97 additions and 87 deletions
+8 -3
View File
@@ -1,13 +1,15 @@
"""The Snooz component."""
from dataclasses import replace
import logging
from pysnooz import parse_snooz_advertisement
from pysnooz.device import SnoozDevice
from homeassistant.components.bluetooth import (
BluetoothReachabilityIntent,
async_address_reachability_diagnostics,
async_ble_device_from_address,
async_last_service_info,
)
from homeassistant.const import CONF_ADDRESS, CONF_TOKEN
from homeassistant.core import HomeAssistant
@@ -25,7 +27,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: SnoozConfigEntry) -> boo
# transitions info logs are verbose. Only enable warnings
logging.getLogger("transitions.core").setLevel(logging.WARNING)
if not (ble_device := async_ble_device_from_address(hass, address)):
if (service_info := async_last_service_info(hass, address)) is None or (
advertisement := parse_snooz_advertisement(service_info)
) is None:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="device_not_found",
@@ -39,7 +43,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: SnoozConfigEntry) -> boo
},
)
device = SnoozDevice(ble_device, token)
ble_device = service_info.device
device = SnoozDevice(ble_device, replace(advertisement, password=token))
entry.runtime_data = SnoozConfigurationData(ble_device, device, entry.title)
+24 -20
View File
@@ -5,7 +5,11 @@ from dataclasses import dataclass
from typing import Any, override
import probatio
from pysnooz.advertisement import SnoozAdvertisementData
from pysnooz import (
SnoozAdvertisementData,
get_device_display_name,
parse_snooz_advertisement,
)
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import (
@@ -30,6 +34,11 @@ class DiscoveredSnooz:
info: BluetoothServiceInfo
device: SnoozAdvertisementData
@property
def display_name(self) -> str:
"""Return the display name."""
return get_device_display_name(self.info.name, self.info.address)
class SnoozConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Snooz."""
@@ -49,8 +58,7 @@ class SnoozConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle the bluetooth discovery step."""
await self.async_set_unique_id(discovery_info.address)
self._abort_if_unique_id_configured()
device = SnoozAdvertisementData()
if not device.supported(discovery_info):
if (device := parse_snooz_advertisement(discovery_info)) is None:
return self.async_abort(reason="not_supported")
self._discovery = DiscoveredSnooz(discovery_info, device)
return await self.async_step_bluetooth_confirm()
@@ -68,8 +76,7 @@ class SnoozConfigFlow(ConfigFlow, domain=DOMAIN):
return self._create_snooz_entry(self._discovery)
self._set_confirm_only()
assert self._discovery.device.display_name
placeholders = {"name": self._discovery.device.display_name}
placeholders = {"name": self._discovery.display_name}
self.context["title_placeholders"] = placeholders
return self.async_show_form(
step_id="bluetooth_confirm", description_placeholders=placeholders
@@ -104,12 +111,9 @@ class SnoozConfigFlow(ConfigFlow, domain=DOMAIN):
address = info.address
if address in configured_addresses:
continue
device = SnoozAdvertisementData()
if device.supported(info):
assert device.display_name
self._discovered_devices[device.display_name] = DiscoveredSnooz(
info, device
)
if (device := parse_snooz_advertisement(info)) is not None:
discovered = DiscoveredSnooz(info, device)
self._discovered_devices[discovered.display_name] = discovered
if not self._discovered_devices:
return self.async_abort(reason="no_devices_found")
@@ -121,10 +125,7 @@ class SnoozConfigFlow(ConfigFlow, domain=DOMAIN):
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=home-assistant-config-flow-name-field
probatio.Required(CONF_NAME): probatio.In(
[
d.device.display_name
for d in self._discovered_devices.values()
]
[d.display_name for d in self._discovered_devices.values()]
)
}
),
@@ -179,24 +180,27 @@ class SnoozConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_show_form(step_id="pairing_timeout")
def _create_snooz_entry(self, discovery: DiscoveredSnooz) -> ConfigFlowResult:
assert discovery.device.display_name
assert discovery.device.password is not None
return self.async_create_entry(
title=discovery.device.display_name,
title=discovery.display_name,
data={
CONF_ADDRESS: discovery.info.address,
CONF_TOKEN: discovery.device.pairing_token,
CONF_TOKEN: discovery.device.password,
},
)
async def _async_wait_for_pairing_mode(self) -> None:
"""Process advertisements until pairing mode is detected."""
assert self._discovery
device = self._discovery.device
def is_device_in_pairing_mode(
service_info: BluetoothServiceInfo,
) -> bool:
return device.supported(service_info) and device.is_pairing
device = parse_snooz_advertisement(service_info)
if device is None or not device.is_pairing:
return False
self._discovery = DiscoveredSnooz(service_info, device)
return True
await async_process_advertisements(
self.hass,
+2 -2
View File
@@ -5,7 +5,7 @@ from datetime import timedelta
from typing import Any, override
import probatio
from pysnooz.api import UnknownSnoozState
from pysnooz import UnknownSnoozState
from pysnooz.commands import (
SnoozCommandData,
SnoozCommandResultStatus,
@@ -139,7 +139,7 @@ class SnoozFan(FanEntity, RestoreEntity):
@override
def assumed_state(self) -> bool:
"""Return True if unable to access real state of the entity."""
return not self._device.is_connected or self._device.state is UnknownSnoozState
return not self._device.is_connected or self._device.state == UnknownSnoozState
@override
async def async_turn_on(
+1 -1
View File
@@ -15,5 +15,5 @@
"documentation": "https://www.home-assistant.io/integrations/snooz",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["pysnooz==0.8.6"]
"requirements": ["pysnooz==0.10.0"]
}
+1 -1
View File
@@ -2663,7 +2663,7 @@ pysmlight==0.5.3
pysnmp==7.1.27
# homeassistant.components.snooz
pysnooz==0.8.6
pysnooz==0.10.0
# homeassistant.components.soma
pysoma==0.0.12
+28 -24
View File
@@ -3,7 +3,7 @@
from dataclasses import dataclass
from unittest.mock import patch
from pysnooz.commands import SnoozCommandData
from pysnooz import SnoozDeviceState, parse_snooz_advertisement, turn_off
from pysnooz.device import DisconnectionReason, SnoozConnectionStatus
from pysnooz.testing import MockSnoozDevice as ParentMockSnoozDevice
@@ -13,12 +13,15 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo
from tests.common import MockConfigEntry
from tests.components.bluetooth import generate_ble_device
from tests.components.bluetooth import (
generate_ble_device,
inject_bluetooth_service_info,
)
TEST_ADDRESS = "00:00:00:00:AB:CD"
TEST_SNOOZ_LOCAL_NAME = "Snooz-ABCD"
TEST_SNOOZ_DISPLAY_NAME = "Snooz ABCD"
TEST_PAIRING_TOKEN = "deadbeef"
TEST_PAIRING_TOKEN = "deadbeefdeadbeef"
NOT_SNOOZ_SERVICE_INFO = BluetoothServiceInfo(
name="Definitely not snooz",
@@ -34,7 +37,7 @@ SNOOZ_SERVICE_INFO_PAIRING = BluetoothServiceInfo(
name=TEST_SNOOZ_LOCAL_NAME,
address=TEST_ADDRESS,
rssi=-63,
manufacturer_data={65552: bytes([4]) + bytes.fromhex(TEST_PAIRING_TOKEN)},
manufacturer_data={65552: bytes([5]) + bytes.fromhex(TEST_PAIRING_TOKEN)},
service_uuids=[
"80c37f00-cc16-11e4-8830-0800200c9a66",
"90759319-1668-44da-9ef3-492d593bd1e5",
@@ -73,7 +76,7 @@ class MockSnoozDevice(ParentMockSnoozDevice):
async def async_disconnect(self) -> None:
"""Disconnect from the device."""
self._is_manually_disconnecting = True
self._expected_disconnect = True
try:
self._cancel_current_command()
if (
@@ -92,21 +95,23 @@ class MockSnoozDevice(ParentMockSnoozDevice):
self._machine.device_disconnected(reason=DisconnectionReason.USER)
finally:
self._is_manually_disconnecting = False
self._expected_disconnect = False
async def create_mock_snooz(
connected: bool = True,
initial_state: SnoozCommandData = SnoozCommandData(on=False, volume=0),
initial_state: SnoozDeviceState = SnoozDeviceState(on=False, volume=0),
) -> MockSnoozDevice:
"""Create a mock device."""
ble_device = SNOOZ_SERVICE_INFO_NOT_PAIRING
device = MockSnoozDevice(ble_device, initial_state=initial_state)
ble_device = generate_ble_device(TEST_ADDRESS, TEST_SNOOZ_LOCAL_NAME)
advertisement = parse_snooz_advertisement(SNOOZ_SERVICE_INFO_PAIRING)
assert advertisement is not None
device = MockSnoozDevice(ble_device, advertisement, initial_state=initial_state)
# execute a command to initiate the connection
if connected is True:
await device.async_execute_command(initial_state)
await device.async_execute_command(turn_off())
return device
@@ -116,21 +121,20 @@ async def create_mock_snooz_config_entry(
) -> MockConfigEntry:
"""Create a mock config entry."""
with (
patch("homeassistant.components.snooz.SnoozDevice", return_value=device),
patch(
"homeassistant.components.snooz.async_ble_device_from_address",
return_value=generate_ble_device(device.address, device.name),
),
):
entry = MockConfigEntry(
domain=DOMAIN,
unique_id=TEST_ADDRESS,
data={CONF_ADDRESS: TEST_ADDRESS, CONF_TOKEN: TEST_PAIRING_TOKEN},
)
entry.add_to_hass(hass)
entry = MockConfigEntry(
domain=DOMAIN,
unique_id=TEST_ADDRESS,
data={CONF_ADDRESS: TEST_ADDRESS, CONF_TOKEN: TEST_PAIRING_TOKEN},
)
entry.add_to_hass(hass)
inject_bluetooth_service_info(hass, SNOOZ_SERVICE_INFO_NOT_PAIRING)
with patch(
"homeassistant.components.snooz.SnoozDevice", return_value=device
) as mock_snooz_device:
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
return entry
assert mock_snooz_device.call_args.args[1].password == TEST_PAIRING_TOKEN
return entry
+28 -32
View File
@@ -1,9 +1,9 @@
"""Test Snooz fan entity."""
from datetime import timedelta
from unittest.mock import Mock, patch
from unittest.mock import Mock, PropertyMock, patch
from pysnooz.api import SnoozDeviceState, UnknownSnoozState
from pysnooz import SnoozDeviceState, UnknownSnoozState
from pysnooz.commands import SnoozCommandResult, SnoozCommandResultStatus
from pysnooz.testing import MockSnoozDevice
import pytest
@@ -30,8 +30,6 @@ from homeassistant.helpers import entity_registry as er
from . import SnoozFixture, create_mock_snooz, create_mock_snooz_config_entry
from tests.components.bluetooth import generate_ble_device
async def test_turn_on(hass: HomeAssistant, snooz_fan_entity_id: str) -> None:
"""Test turning on the device."""
@@ -200,13 +198,7 @@ async def test_restore_state(
assert state.state == STATE_UNAVAILABLE
# reload entry
with (
patch("homeassistant.components.snooz.SnoozDevice", return_value=device),
patch(
"homeassistant.components.snooz.async_ble_device_from_address",
return_value=generate_ble_device(device.address, device.name),
),
):
with patch("homeassistant.components.snooz.SnoozDevice", return_value=device):
await hass.config_entries.async_setup(entry.entry_id)
# should match last known state
@@ -232,13 +224,7 @@ async def test_restore_unknown_state(
assert state.state == STATE_UNAVAILABLE
# reload entry
with (
patch("homeassistant.components.snooz.SnoozDevice", return_value=device),
patch(
"homeassistant.components.snooz.async_ble_device_from_address",
return_value=generate_ble_device(device.address, device.name),
),
):
with patch("homeassistant.components.snooz.SnoozDevice", return_value=device):
await hass.config_entries.async_setup(entry.entry_id)
# should match last known state
@@ -257,14 +243,19 @@ async def test_command_results(
mock_execute.return_value = SnoozCommandResult(
SnoozCommandResultStatus.SUCCESSFUL, timedelta()
)
mock_connected_snooz.device.state = SnoozDeviceState(on=True, volume=56)
await hass.services.async_call(
fan.DOMAIN,
fan.SERVICE_TURN_ON,
{ATTR_ENTITY_ID: [snooz_fan_entity_id]},
blocking=True,
)
with patch.object(
MockSnoozDevice,
"state",
new_callable=PropertyMock,
return_value=SnoozDeviceState(on=True, volume=56),
):
await hass.services.async_call(
fan.DOMAIN,
fan.SERVICE_TURN_ON,
{ATTR_ENTITY_ID: [snooz_fan_entity_id]},
blocking=True,
)
state = hass.states.get(snooz_fan_entity_id)
assert state.state == STATE_ON
@@ -273,14 +264,19 @@ async def test_command_results(
mock_execute.return_value = SnoozCommandResult(
SnoozCommandResultStatus.CANCELLED, timedelta()
)
mock_connected_snooz.device.state = SnoozDeviceState(on=False, volume=15)
await hass.services.async_call(
fan.DOMAIN,
fan.SERVICE_TURN_ON,
{ATTR_ENTITY_ID: [snooz_fan_entity_id]},
blocking=True,
)
with patch.object(
MockSnoozDevice,
"state",
new_callable=PropertyMock,
return_value=SnoozDeviceState(on=False, volume=15),
):
await hass.services.async_call(
fan.DOMAIN,
fan.SERVICE_TURN_ON,
{ATTR_ENTITY_ID: [snooz_fan_entity_id]},
blocking=True,
)
# the device state shouldn't be written when cancelled
state = hass.states.get(snooz_fan_entity_id)
+5 -4
View File
@@ -16,11 +16,11 @@ from . import TEST_ADDRESS, TEST_PAIRING_TOKEN, SnoozFixture
from tests.common import MockConfigEntry
async def test_setup_retries_when_device_not_found(
async def test_setup_retries_when_service_info_not_found(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test setup is retried with a diagnostic reason when the device is missing."""
"""Test setup is retried when Bluetooth service info is missing."""
entry = MockConfigEntry(
domain=DOMAIN,
unique_id=TEST_ADDRESS,
@@ -30,9 +30,9 @@ async def test_setup_retries_when_device_not_found(
with (
patch(
"homeassistant.components.snooz.async_ble_device_from_address",
"homeassistant.components.snooz.async_last_service_info",
return_value=None,
),
) as mock_last_service_info,
patch(
"homeassistant.components.snooz.async_address_reachability_diagnostics",
return_value="mock reachability reason",
@@ -41,6 +41,7 @@ async def test_setup_retries_when_device_not_found(
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
mock_last_service_info.assert_called_once_with(hass, TEST_ADDRESS)
assert entry.state is ConfigEntryState.SETUP_RETRY
assert (
f"Could not find Snooz with address {TEST_ADDRESS}: mock reachability reason"