Skip to content

Commit

Permalink
merged master
Browse files Browse the repository at this point in the history
  • Loading branch information
Christopher Hoch committed Sep 16, 2024
2 parents 795b3ed + 347025a commit b3437b2
Show file tree
Hide file tree
Showing 27 changed files with 82 additions and 72 deletions.
21 changes: 11 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ jobs:
matrix:
python-version: ["3.11", "3.12"]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Cache pip packages
uses: actions/cache@v2
uses: actions/cache@v4
env:
cache-name: cache-pypi-modules
with:
Expand All @@ -31,7 +31,7 @@ jobs:
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
- name: Cache pre-commit packages
uses: actions/cache@v2
uses: actions/cache@v4
env:
cache-name: cache-pre-commit
with:
Expand All @@ -55,10 +55,11 @@ jobs:
- name: Isort
run: isort --check-only test examples pyvlx
- name: Upload coverage artifact
uses: actions/upload-artifact@v2.2.2
uses: actions/upload-artifact@v4
with:
name: coverage-${{ matrix.python-version }}
path: .coverage
include-hidden-files: true

coverage:
name: Process test coverage
Expand All @@ -68,13 +69,13 @@ jobs:
matrix:
python-version: ["3.11"]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Cache pip packages
uses: actions/cache@v2
uses: actions/cache@v4
env:
cache-name: cache-pypi-modules
with:
Expand All @@ -84,7 +85,7 @@ jobs:
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
- name: Cache pre-commit packages
uses: actions/cache@v2
uses: actions/cache@v4
env:
cache-name: cache-pre-commit
with:
Expand All @@ -97,14 +98,14 @@ jobs:
run: |
pip install -r requirements/testing.txt
- name: Download all coverage artifacts
uses: actions/download-artifact@v2
uses: actions/download-artifact@v4
- name: Create coverage report
run: |
coverage combine coverage*/.coverage*
coverage report --fail-under=79
coverage xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
19 changes: 5 additions & 14 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,7 @@
{
"files.associations": {
"*.yaml": "home-assistant"
},
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": false
},
},
"isort.args":["--profile", "black"],
"python.testing.pytestArgs": [],
"python.testing.pytestArgs": [
"test"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
}
"python.testing.pytestEnabled": true
}
39 changes: 21 additions & 18 deletions pyvlx/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,16 @@ class TCPTransport(asyncio.Protocol):
def __init__(
self,
frame_received_cb: Callable[[FrameBase], None],
connection_closed_cb: Callable[[], None],
connection_lost_cb: Callable[[], None],
):
"""Init TCPTransport."""
self.frame_received_cb = frame_received_cb
self.connection_closed_cb = connection_closed_cb
self.connection_lost_cb = connection_lost_cb
self.tokenizer = SlipTokenizer()

def connection_made(self, transport: object) -> None:
"""Handle sucessful connection."""
PYVLXLOG.debug("Socket connection to KLF 200 opened")

def data_received(self, data: bytes) -> None:
"""Handle data received."""
Expand All @@ -67,7 +68,8 @@ def data_received(self, data: bytes) -> None:

def connection_lost(self, exc: object) -> None:
"""Handle lost connection."""
self.connection_closed_cb()
PYVLXLOG.debug("Socket connection to KLF 200 has been lost")
self.connection_lost_cb()


CallbackType = Callable[[FrameBase], Coroutine]
Expand All @@ -86,6 +88,7 @@ def __init__(self, loop: asyncio.AbstractEventLoop, config: Config):
self.connection_opened_cbs: List[Callable[[], Coroutine]] = []
self.connected = False
self.connection_counter = 0
self.tasks: List[asyncio.Task] = []

def __del__(self) -> None:
"""Destruct connection."""
Expand All @@ -97,13 +100,15 @@ def disconnect(self) -> None:
self.transport.close()
self.transport = None
self.connected = False
PYVLXLOG.debug("TCP transport closed.")
for connection_closed_cb in self.connection_closed_cbs:
# pylint: disable=not-callable
self.loop.create_task(connection_closed_cb())
if asyncio.iscoroutine(connection_closed_cb()):
task = self.loop.create_task(connection_closed_cb())
self.tasks.append(task)

async def connect(self) -> None:
"""Connect to gateway via SSL."""
tcp_client = TCPTransport(self.frame_received_cb, connection_closed_cb=self.connection_closed_cb)
tcp_client = TCPTransport(self.frame_received_cb, connection_lost_cb=self.on_connection_lost)
assert self.config.host is not None
self.transport, _ = await self.loop.create_connection(
lambda: tcp_client,
Expand All @@ -117,8 +122,9 @@ async def connect(self) -> None:
"Amount of connections since last HA start: %s", self.connection_counter
)
for connection_opened_cb in self.connection_opened_cbs:
# pylint: disable=not-callable
await self.loop.create_task(connection_opened_cb())
if asyncio.iscoroutine(connection_opened_cb()):
task = self.loop.create_task(connection_opened_cb())
self.tasks.append(task)

def register_frame_received_cb(self, callback: CallbackType) -> None:
"""Register frame received callback."""
Expand All @@ -129,23 +135,19 @@ def unregister_frame_received_cb(self, callback: CallbackType) -> None:
self.frame_received_cbs.remove(callback)

def register_connection_closed_cb(self, callback: Callable[[], Coroutine]) -> None:
"""Register frame received callback."""
"""Register connection closed callback."""
self.connection_closed_cbs.append(callback)
if not self.connected:
self.loop.create_task(callback())

def unregister_connection_closed_cb(self, callback: Callable[[], Coroutine]) -> None:
"""Unregister frame received callback."""
"""Unregister connection closed callback."""
self.connection_closed_cbs.remove(callback)

def register_connection_opened_cb(self, callback: Callable[[], Coroutine]) -> None:
"""Register frame received callback."""
"""Register connection opened callback."""
self.connection_opened_cbs.append(callback)
if self.connected:
self.loop.create_task(callback())

def unregister_connection_opened_cb(self, callback: Callable[[], Coroutine]) -> None:
"""Unregister frame received callback."""
"""Unregister connection opened callback."""
self.connection_opened_cbs.remove(callback)

def write(self, frame: FrameBase) -> None:
Expand All @@ -169,8 +171,9 @@ def frame_received_cb(self, frame: FrameBase) -> None:
PYVLXLOG.debug("REC: %s", frame)
for frame_received_cb in self.frame_received_cbs:
# pylint: disable=not-callable
self.loop.create_task(frame_received_cb(frame))
task = self.loop.create_task(frame_received_cb(frame))
self.tasks.append(task)

def connection_closed_cb(self) -> None:
def on_connection_lost(self) -> None:
"""Server closed connection."""
self.disconnect()
4 changes: 3 additions & 1 deletion pyvlx/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ def __init__(self, pyvlx: "PyVLX", node_id: int, name: str, serial_number: Optio
self.name = name
self.serial_number = serial_number
self.device_updated_cbs: List[CallbackType] = []
self.pyvlx.connection.register_connection_opened_cb(self.after_update)
self.pyvlx.connection.register_connection_closed_cb(self.after_update)

def register_device_updated_cb(self, device_updated_cb: CallbackType) -> None:
"""Register device updated callback."""
Expand All @@ -41,7 +43,7 @@ async def after_update(self) -> None:
PYVLXLOG.debug("Node %r after update. Calling %d update listeners", self.node_id, len(self.device_updated_cbs))
for device_updated_cb in self.device_updated_cbs:
# pylint: disable=not-callable
self.pyvlx.loop.create_task(device_updated_cb(self)) # type: ignore
await self.pyvlx.loop.create_task(device_updated_cb(self)) # type: ignore

async def rename(self, name: str) -> None:
"""Change name of node."""
Expand Down
14 changes: 14 additions & 0 deletions pyvlx/opening_device.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Module for Opening devices."""
import asyncio
import datetime
from asyncio import Task
from typing import TYPE_CHECKING, Any, Optional

from .api.command_send import CommandSend
Expand Down Expand Up @@ -50,6 +52,16 @@ def __init__(
self.default_velocity: Velocity = Velocity.DEFAULT
self.open_position_target: int = 0
self.close_position_target: int = 100
self._update_task: Task | None = None

async def _update_calls(self) -> None:
"""While cover are moving, perform periodically update calls."""
while self.is_moving():
await asyncio.sleep(1)
await self.after_update()
if self._update_task:
self._update_task.cancel()
self._update_task = None

async def set_position(
self,
Expand Down Expand Up @@ -176,6 +188,8 @@ def get_position(self) -> Position:
current_position = (
movement_origin + (movement_target - movement_origin) / 100 * percent
)
if not self._update_task:
self._update_task = self.pyvlx.loop.create_task(self._update_calls())
return Position(position_percent=int(current_position))
return self.position

Expand Down
23 changes: 13 additions & 10 deletions pyvlx/pyvlx.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ def __init__(
self.loop = loop or asyncio.get_event_loop()
self.config = Config(self, path, host, password)
self.connection = Connection(loop=self.loop, config=self.config)
self.connection.register_connection_closed_cb(self.on_connection_closed_cb)
self.heartbeat = Heartbeat(
pyvlx=self,
interval=heartbeat_interval,
Expand All @@ -46,12 +45,13 @@ def __init__(
self.node_updater = NodeUpdater(pyvlx=self)
self.nodes = Nodes(self)
self.connection.register_frame_received_cb(self.node_updater.process_frame)

self.scenes = Scenes(self)
self.version = None
self.protocol_version = None
self.klf200 = Klf200Gateway(pyvlx=self)
self.api_call_semaphore = asyncio.Semaphore(1) # Limit parallel commands
PYVLXLOG.debug("Loadig pyvlx v0.2.23")
PYVLXLOG.debug("Loadig pyvlx v0.2.24")

async def connect(self) -> None:
"""Connect to KLF 200."""
Expand Down Expand Up @@ -101,15 +101,18 @@ async def send_frame(self, frame: FrameBase) -> None:

async def disconnect(self) -> None:
"""Disconnect from KLF 200."""
# If the connection will be closed while house status monitor is enabled, a reconnection will fail on SSL handshake.
try:
await self.klf200.house_status_monitor_disable(pyvlx=self, timeout=1)
except (OSError, PyVLXException):
pass
await self.heartbeat.stop()
# Reboot KLF200 when disconnecting to avoid unresponsive KLF200.
await self.klf200.reboot()
self.connection.disconnect()
if self.connection.connected:
try:
# If the connection will be closed while house status monitor is enabled, a reconnection will fail on SSL handshake.
await self.klf200.house_status_monitor_disable(pyvlx=self, timeout=1)
# Reboot KLF200 when disconnecting to avoid unresponsive KLF200.
await self.klf200.reboot()
except (OSError, PyVLXException):
pass
self.connection.disconnect()
if self.connection.tasks:
await asyncio.gather(*self.connection.tasks)

async def load_nodes(self, node_id: Optional[int] = None) -> None:
"""Load devices from KLF 200, if no node_id is specified all nodes are loaded."""
Expand Down
2 changes: 1 addition & 1 deletion requirements/testing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ flake8==7.1.1
flake8-isort==6.1.1
pydocstyle==6.3.0
pylint==3.2.7
pytest==8.3.2
pytest==8.3.3
pytest-cov==5.0.0
pytest-timeout==2.3.1
setuptools==74.1.2
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

PKG_ROOT = os.path.dirname(__file__)

VERSION = "0.2.23"
VERSION = "0.2.24"


def get_long_description() -> str:
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
2 changes: 1 addition & 1 deletion test/frame_get_local_time_cfm_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def test_bytes(self) -> None:
def test_frame_from_raw(self) -> None:
"""Test parse FrameGetLocalTimeConfirmation from raw."""
frame = frame_from_raw(
b"\x00\x12 \x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x007"
b"\x00\x12 \x05_\xc9,'\x13\x13\x12\x03\x0c\x00x\x04\x01R\xffg"
)
self.assertTrue(isinstance(frame, FrameGetLocalTimeConfirmation))

Expand Down
3 changes: 3 additions & 0 deletions test/lightening_device_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from unittest.mock import MagicMock

from pyvlx import Light, PyVLX
from pyvlx.connection import Connection


# pylint: disable=too-many-public-methods,invalid-name
Expand All @@ -12,6 +13,8 @@ class TestLighteningDevice(unittest.TestCase):
def setUp(self) -> None:
"""Set up TestGetLimitation."""
self.pyvlx = MagicMock(spec=PyVLX)
connection = MagicMock(spec=Connection)
self.pyvlx.attach_mock(mock=connection, attribute="connection")

def test_light_str(self) -> None:
"""Test string representation of Light object."""
Expand Down
3 changes: 3 additions & 0 deletions test/node_helper_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from pyvlx import (
Blade, Blind, GarageDoor, Gate, Light, PyVLX, RollerShutter, Window)
from pyvlx.api.frames import FrameGetNodeInformationNotification
from pyvlx.connection import Connection
from pyvlx.const import NodeTypeWithSubtype
from pyvlx.node_helper import convert_frame_to_node

Expand All @@ -17,6 +18,8 @@ class TestNodeHelper(unittest.TestCase):
def setUp(self) -> None:
"""Set up TestNodeHelper."""
self.pyvlx = MagicMock(spec=PyVLX)
connection = MagicMock(spec=Connection)
self.pyvlx.attach_mock(mock=connection, attribute="connection")

def test_window(self) -> None:
"""Test convert_frame_to_node with window."""
Expand Down
3 changes: 3 additions & 0 deletions test/nodes_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from unittest.mock import MagicMock

from pyvlx import Blind, Nodes, PyVLX, RollerShutter, Window
from pyvlx.connection import Connection
from pyvlx.node import Node

# pylint: disable=too-many-public-methods,invalid-name
Expand All @@ -14,6 +15,8 @@ class TestNodes(unittest.TestCase):
def setUp(self) -> None:
"""Set up TestNodes."""
self.pyvlx = MagicMock(spec=PyVLX)
connection = MagicMock(spec=Connection)
self.pyvlx.attach_mock(mock=connection, attribute="connection")

def test_get_item(self) -> None:
"""Test get_item."""
Expand Down
Loading

0 comments on commit b3437b2

Please sign in to comment.