mirror of
https://github.com/home-assistant/core.git
synced 2026-09-26 09:23:17 -04:00
Bump pysnooz to 0.10.0 (#182225)
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
Generated
+1
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user