Skip to content

Commit

Permalink
Handle updates sent via mDNS (#96)
Browse files Browse the repository at this point in the history
  • Loading branch information
Shutgun authored Dec 20, 2022
1 parent d768d41 commit 145c0f4
Show file tree
Hide file tree
Showing 3 changed files with 39 additions and 41 deletions.
53 changes: 32 additions & 21 deletions devolo_plc_api/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,6 @@ def __init__(
deviceapi: dict[str, Any] | None = None,
zeroconf_instance: AsyncZeroconf | Zeroconf | None = None,
) -> None:
self.firmware_date = date.fromtimestamp(0)
self.firmware_version = ""
self.hostname = ""
self.ip = ip
self.mac = ""
self.mt_number = 0
Expand All @@ -54,6 +51,7 @@ def __init__(
self.device: DeviceApi | None = None
self.plcnet: PlcNetApi | None = None

self._browser: dict[str, AsyncServiceBrowser] = {}
self._connected = False
self._info: dict[str, dict[str, Any]] = {
PLCNETAPI: plcnetapi or EMPTY_INFO,
Expand Down Expand Up @@ -87,6 +85,25 @@ def __enter__(self) -> Device:
def __exit__(self, exc_type: type | None, exc_val: BaseException | None, exc_tb: TracebackType | None) -> None:
self.disconnect()

@property
def firmware_date(self) -> date:
"""Date the firmware was built."""
if DEVICEAPI in self._info:
return date.fromisoformat(self._info[DEVICEAPI]["properties"].get("FirmwareDate", "1970-01-01")[:10])
return date.fromtimestamp(0)

@property
def firmware_version(self) -> str:
if DEVICEAPI in self._info:
return self._info[DEVICEAPI]["properties"].get("FirmwareVersion", "")
return ""

@property
def hostname(self) -> str:
if DEVICEAPI in self._info:
return self._info[DEVICEAPI].get("hostname", "")
return ""

@property
def password(self) -> str:
"""The currently set device password."""
Expand Down Expand Up @@ -124,11 +141,14 @@ def connect(self) -> None:

async def async_disconnect(self) -> None:
"""Disconnect from a device asynchronous."""
if not self._zeroconf_instance:
await self._zeroconf.async_close()
if not self._session_instance:
await self._session.aclose()
self._connected = False
if self._connected:
for browser in self._browser.values():
await browser.async_cancel()
if not self._zeroconf_instance:
await self._zeroconf.async_close()
if not self._session_instance:
await self._session.aclose()
self._connected = False

def disconnect(self) -> None:
"""Disconnect from a device synchronous."""
Expand All @@ -141,11 +161,6 @@ async def _get_device_info(self) -> None:
if not self._info[service_type]["properties"]:
await self._retry_zeroconf_info(service_type=service_type)
if self._info[service_type]["properties"]:
self.firmware_date = date.fromisoformat(
self._info[service_type]["properties"].get("FirmwareDate", "1970-01-01")[:10]
)
self.firmware_version = self._info[service_type]["properties"].get("FirmwareVersion", "")
self.hostname = self._info[service_type].get("hostname", "")
self.mt_number = self._info[service_type]["properties"].get("MT", 0)
self.product = self._info[service_type]["properties"].get("Product", "")
self.serial_number = self._info[service_type]["properties"]["SN"]
Expand All @@ -167,14 +182,11 @@ async def _get_plcnet_info(self) -> None:

async def _get_zeroconf_info(self, service_type: str) -> None:
"""Browse for the desired mDNS service types and query them."""
if self._info[service_type]["properties"]:
return # No need to continue, if device info already exist

self._logger.debug("Browsing for %s", service_type)
counter = 0
addr = None if self._multicast else self.ip
question_type = DNSQuestionType.QM if self._multicast else DNSQuestionType.QU
browser = AsyncServiceBrowser(
self._browser[service_type] = AsyncServiceBrowser(
zeroconf=self._zeroconf.zeroconf,
type_=service_type,
handlers=[self._state_change],
Expand All @@ -184,7 +196,6 @@ async def _get_zeroconf_info(self, service_type: str) -> None:
while not self._info[service_type]["properties"] and counter < 300:
counter += 1
await asyncio.sleep(0.01)
await browser.async_cancel()

async def _retry_zeroconf_info(self, service_type: str) -> None:
"""Retry getting the zeroconf info using multicast."""
Expand All @@ -196,9 +207,9 @@ async def _retry_zeroconf_info(self, service_type: str) -> None:

def _state_change(self, zeroconf: Zeroconf, service_type: str, name: str, state_change: ServiceStateChange) -> None:
"""Evaluate the query result."""
if state_change is not ServiceStateChange.Added:
if state_change == ServiceStateChange.Removed:
return
asyncio.ensure_future(self._get_service_info(zeroconf, service_type, name))
asyncio.create_task(self._get_service_info(zeroconf, service_type, name))

async def _get_service_info(self, zeroconf: Zeroconf, service_type: str, name: str) -> None:
"""Get service information, if IP matches."""
Expand All @@ -214,7 +225,7 @@ async def _get_service_info(self, zeroconf: Zeroconf, service_type: str, name: s
):
return # No need to continue, if there are no relevant service information

self._logger.debug("Adding service info of %s to %s", service_type, service_info.server_key)
self._logger.debug("Updating service info of %s for %s", service_type, service_info.server_key)
self._info[service_type] = self.info_from_service(service_info)

@staticmethod
Expand Down
6 changes: 6 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [v0.9.0] - 2022/12/20

### Added

- Handle updates sent via mDNS

## [v0.8.1] - 2022/10/18

### Fixed
Expand Down
21 changes: 1 addition & 20 deletions tests/test_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,14 +173,6 @@ async def test__get_zeroconf_info_timeout(self, mock_device: Device):
await mock_device._get_zeroconf_info("_http._tcp.local.")
assert sleep.call_count == 300

@pytest.mark.asyncio
async def test__get_zeroconf_info(self, test_data: TestData, mock_service_browser: MockServiceBrowser):
"""Test that after getting zeroconf information browsing is stopped."""
with patch("devolo_plc_api.device.Device._state_change", state_change):
mock_device = Device(ip=test_data.ip, plcnetapi=None, deviceapi=test_data.device_info[DEVICEAPI])
await mock_device.async_connect()
assert mock_service_browser.async_cancel.call_count == 1

@pytest.mark.asyncio
async def test__get_zeroconf_info_device_info_exists(self, mock_device: Device, mock_service_browser: MockServiceBrowser):
"""Test early return if information already exist."""
Expand All @@ -198,17 +190,6 @@ async def test__state_change_no_service_info(self, test_data: TestData):
await mock_device.async_connect()
assert ifs.call_count == 0

@pytest.mark.asyncio
@pytest.mark.usefixtures("mock_service_browser")
async def test__state_change_added(self, test_data: TestData):
"""Test that service information are processed on state change to added."""
with patch("devolo_plc_api.device.Device._get_service_info") as gsi, patch(
"devolo_plc_api.device.Device._retry_zeroconf_info"
), patch("asyncio.sleep"):
mock_device = Device(ip=test_data.ip, plcnetapi=None, deviceapi=test_data.device_info[DEVICEAPI])
await mock_device.async_connect()
assert gsi.call_count == 1

@pytest.mark.asyncio
# pylint: disable=protected-access
async def test__state_change_removed(self, mock_device: Device):
Expand All @@ -228,7 +209,7 @@ async def test__get_service_info(self, test_data: TestData):
), patch("devolo_plc_api.device.AsyncServiceInfo.async_request") as ar:
mock_device = Device(ip=test_data.ip, plcnetapi=None, deviceapi=test_data.device_info[DEVICEAPI])
await mock_device.async_connect()
assert ar.call_count == 1
assert ar.call_count == 2
assert mock_device.mac == test_data.device_info[PLCNETAPI]["properties"]["PlcMacAddress"]

def test_info_from_service_no_address(self, mock_device: Device):
Expand Down

0 comments on commit 145c0f4

Please sign in to comment.