Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update to support zigpy 0.70.0 #58

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ license = GPL-3.0
packages = find:
python_requires = >=3.7
install_requires =
zigpy>=0.60.2
zigpy>=0.70.0
async_timeout
voluptuous
coloredlogs
Expand Down
13 changes: 3 additions & 10 deletions zigpy_zboss/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,6 @@
LISTENER_LOGGER.propagate = False

# All of these are in seconds
AFTER_BOOTLOADER_SKIP_BYTE_DELAY = 2.5
NETWORK_COMMISSIONING_TIMEOUT = 30
BOOTLOADER_PIN_TOGGLE_DELAY = 0.15
CONNECT_PING_TIMEOUT = 0.50
CONNECT_PROBE_TIMEOUT = 10

DEFAULT_TIMEOUT = 5

EXPECTED_DISCONNECT_TIMEOUT = 5.0
Expand Down Expand Up @@ -87,7 +81,7 @@ async def connect(self) -> None:
except (Exception, asyncio.CancelledError):
LOGGER.debug(
"Connection to %s failed, cleaning up", self._port_path)
self.close()
await self.disconnect()
raise

LOGGER.debug(
Expand All @@ -106,7 +100,7 @@ def connection_lost(self, exc) -> None:
if self._app is not None and not self._reset_uart_reconnect.locked():
self._app.connection_lost(exc)

def close(self) -> None:
async def disconnect(self) -> None:
"""Clean up resources, namely the listener queues.

Calling this will reset ZBOSS to the same internal state as a fresh
Expand All @@ -122,7 +116,7 @@ def close(self) -> None:
self._listeners.clear()

if self._uart is not None:
self._uart.close()
await self._uart.disconnect()
self._uart = None

def frame_received(self, frame: Frame) -> bool:
Expand Down Expand Up @@ -347,7 +341,6 @@ async def reset(

tsn = self._app.get_sequence() if self._app is not None else 0
req = c.NcpConfig.NCPModuleReset.Req(TSN=tsn, Option=option)
self._uart.reset_flag = True

async with self._reset_uart_reconnect:
await self._send_to_uart(req.to_frame())
Expand Down
76 changes: 1 addition & 75 deletions zigpy_zboss/config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"""Module responsible for configuration."""
import numbers
import typing

import voluptuous as vol
Expand All @@ -16,82 +15,9 @@

ConfigType = typing.Dict[str, typing.Any]

VolPositiveNumber = vol.All(numbers.Real, vol.Range(min=0))

CONF_DEVICE_BAUDRATE = "baudrate"
CONF_DEVICE_FLOW_CONTROL = "flow_control"
CONF_DEVICE_BAUDRATE_DEFAULT = 115_200
CONF_DEVICE_FLOW_CONTROL_DEFAULT = None

SCHEMA_DEVICE = SCHEMA_DEVICE.extend(
{
vol.Optional(
CONF_DEVICE_BAUDRATE, default=CONF_DEVICE_BAUDRATE_DEFAULT): int,
vol.Optional(
CONF_DEVICE_FLOW_CONTROL,
default=CONF_DEVICE_FLOW_CONTROL_DEFAULT): vol.In(
("hardware", "software", None)
),
}
)


def keys_have_same_length(*keys):
"""Raise an error if values don't have the same length."""
def validator(config):
lengths = [len(config[k]) for k in keys]

if len(set(lengths)) != 1:
raise vol.Invalid(
f"Values for {keys} must all have the same length: {lengths}"
)

return config

return validator


CONF_ZBOSS_CONFIG = "zboss_config"
CONF_TX_POWER = "tx_power"
CONF_LED_MODE = "led_mode"
CONF_SKIP_BOOTLOADER = "skip_bootloader"
CONF_REQ_TIMEOUT = "request_timeout"
CONF_AUTO_RECONNECT_RETRY_DELAY = "auto_reconnect_retry_delay"
CONF_MAX_CONCURRENT_REQUESTS = "max_concurrent_requests"
CONF_CONNECT_RTS_STATES = "connect_rts_pin_states"
CONF_CONNECT_DTR_STATES = "connect_dtr_pin_states"

CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
{
vol.Required(CONF_DEVICE): SCHEMA_DEVICE,
vol.Optional(CONF_ZBOSS_CONFIG, default={}): vol.Schema(
vol.All(
{
vol.Optional(CONF_TX_POWER, default=None): vol.Any(
None, vol.All(int, vol.Range(min=-22, max=22))
),
vol.Optional(
CONF_REQ_TIMEOUT, default=15): VolPositiveNumber,
vol.Optional(
CONF_AUTO_RECONNECT_RETRY_DELAY, default=5
): VolPositiveNumber,
vol.Optional(
CONF_SKIP_BOOTLOADER, default=True): cv_boolean,
vol.Optional(CONF_LED_MODE, default=None): vol.Any(None),
vol.Optional(
CONF_MAX_CONCURRENT_REQUESTS, default="auto"): vol.Any(
"auto", VolPositiveNumber
),
vol.Optional(
CONF_CONNECT_RTS_STATES, default=[False, True, False]
): vol.Schema([cv_boolean]),
vol.Optional(
CONF_CONNECT_DTR_STATES, default=[False, False, False]
): vol.Schema([cv_boolean]),
},
keys_have_same_length(
CONF_CONNECT_RTS_STATES, CONF_CONNECT_DTR_STATES),
)
),
vol.Optional(CONF_ZBOSS_CONFIG, default={}): vol.Schema(),
}
)
102 changes: 12 additions & 90 deletions zigpy_zboss/uart.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,93 +15,38 @@

LOGGER = logging.getLogger(__name__)
ACK_TIMEOUT = 1
SEND_RETRIES = 2
STARTUP_TIMEOUT = 5
RECONNECT_TIMEOUT = 10


class BufferTooShort(Exception):
"""Exception when the buffer is too short."""


class ZbossNcpProtocol(asyncio.Protocol):
class ZbossNcpProtocol(zigpy.serial.SerialProtocol):
"""Zboss Ncp Protocol class."""

def __init__(self, config, api) -> None:
def __init__(self, api) -> None:
"""Initialize the ZbossNcpProtocol object."""
super().__init__()
self._api = api
self._ack_seq = 0
self._pack_seq = 0
self._config = config
self._transport = None
self._reset_flag = False
self._buffer = bytearray()
self._reconnect_task = None
self._tx_lock = asyncio.Lock()
self._ack_received_event = None
self._connected_event = asyncio.Event()

self._port = config[conf.CONF_DEVICE_PATH]
self._baudrate = config[conf.CONF_DEVICE_BAUDRATE]
self._flow_control = config[conf.CONF_DEVICE_FLOW_CONTROL]

@property
def api(self):
"""Return the owner of that object."""
return self._api

@property
def name(self) -> str:
"""Return serial name."""
return self._transport.serial.name

@property
def baudrate(self) -> int:
"""Return the baudrate."""
return self._transport.serial.baudrate

@property
def reset_flag(self) -> bool:
"""Return True if a reset is in process."""
return self._reset_flag

@reset_flag.setter
def reset_flag(self, value) -> None:
if isinstance(value, bool):
self._reset_flag = value

def connection_made(
self, transport: asyncio.BaseTransport) -> None:
"""Notify serial port opened."""
self._transport = transport
message = f"Opened {transport.serial.name} serial port"
if self._reset_flag:
self._reset_flag = False
return
SERIAL_LOGGER.info(message)
self._connected_event.set()

def connection_lost(self, exc: typing.Optional[Exception]) -> None:
"""Lost connection."""
LOGGER.debug("Connection has been lost: %r", exc)

super().connection_lost(exc)
if self._api is not None:
self._api.connection_lost(exc)

def close(self) -> None:
"""Close serial connection."""
self._buffer.clear()
super().close()
self._api = None
self._ack_seq = 0
self._pack_seq = 0

# Reset transport
if self._transport:
message = "Closing serial port"
LOGGER.debug(message)
SERIAL_LOGGER.info(message)
self._transport.close()
self._transport = None

def write(self, data: bytes) -> None:
"""Write raw bytes to the transport.

Expand Down Expand Up @@ -226,41 +171,18 @@ def _ack_frame(self):
ack_frame = Frame.ack(self._ack_seq)
return ack_frame

def __repr__(self) -> str:
"""Return a string representing the class."""
return (
f"<"
f"{type(self).__name__} connected to {self.name!r}"
f" at {self.baudrate} baud"
f" (api: {self._api})"
f">"
)


async def connect(config: conf.ConfigType, api) -> ZbossNcpProtocol:
"""Instantiate Uart object and connect to it."""
loop = asyncio.get_running_loop()

port = config[conf.CONF_DEVICE_PATH]
baudrate = config[conf.CONF_DEVICE_BAUDRATE]
flow_control = config[conf.CONF_DEVICE_FLOW_CONTROL]
port = config[zigpy.config.CONF_DEVICE_PATH]

_, protocol = await zigpy.serial.create_serial_connection(
loop=loop,
protocol_factory=lambda: ZbossNcpProtocol(config, api),
loop=asyncio.get_running_loop(),
protocol_factory=lambda: ZbossNcpProtocol(api),
url=port,
baudrate=baudrate,
xonxoff=(flow_control == "software"),
rtscts=(flow_control == "hardware"),
baudrate=config[zigpy.config.CONF_DEVICE_BAUDRATE],
flow_control=config[zigpy.config.CONF_DEVICE_FLOW_CONTROL],
)

try:
async with async_timeout.timeout(STARTUP_TIMEOUT):
await protocol._connected_event.wait()
except asyncio.TimeoutError:
protocol.close()
raise RuntimeError("Could not communicate with NCP!")

LOGGER.debug("Connected to %s at %s baud", port, baudrate)
await protocol.wait_until_connected()

return protocol
9 changes: 4 additions & 5 deletions zigpy_zboss/zigbee/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import zigpy_zboss.types as t_zboss
from zigpy_zboss import commands as c
from zigpy_zboss.api import ZBOSS
from zigpy_zboss.config import CONFIG_SCHEMA, SCHEMA_DEVICE
from zigpy_zboss.config import CONFIG_SCHEMA

from .device import ZbossCoordinator, ZbossDevice

Expand All @@ -37,7 +37,6 @@ class ControllerApplication(zigpy.application.ControllerApplication):
"""Controller class."""

SCHEMA = CONFIG_SCHEMA
SCHEMA_DEVICE = SCHEMA_DEVICE

def __init__(self, config: Dict[str, Any]):
"""Initialize instance."""
Expand All @@ -56,7 +55,7 @@ async def connect(self):
c.NcpConfig.GetZigbeeRole.Req(TSN=1), timeout=1
)
except Exception:
zboss.close()
await zboss.disconnect()
raise

self._api = zboss
Expand All @@ -73,7 +72,7 @@ async def disconnect(self):
"Failed to reset API during disconnect", exc_info=True
)

self._api.close()
await self._api.disconnect()
self._api = None

async def start_network(self):
Expand Down Expand Up @@ -646,7 +645,7 @@ async def send_packet(self, packet: t.ZigbeePacket) -> None:
# Don't release the concurrency-limiting semaphore until we are done
# trying. There is no point in allowing requests to take turns getting
# buffer errors.
async with self._limit_concurrency():
async with self._limit_concurrency(priority=packet.priority):
await self._api.request(
c.APS.DataReq.Req(
TSN=packet.tsn,
Expand Down