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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Typecheck Devices #551

Merged
merged 14 commits into from
Jan 6, 2021
4 changes: 2 additions & 2 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,15 @@ warn_unused_configs = true

# add the modules below once we add typing for them so that we fail the build in the future if someone changes something without updating the typings
# fully typechecked modules
[mypy-xknx.xknx,xknx.core.*,xknx.exceptions.*,xknx.io.*,xknx.knxip.*,xknx.telegram.*,]
[mypy-xknx.xknx,xknx.core.*,xknx.devices.*,xknx.exceptions.*,xknx.io.*,xknx.knxip.*,xknx.telegram.*,]
strict = true
ignore_errors = false
warn_unreachable = true
# TODO: turn these off, address issues
implicit_reexport = true

# partly typechecked modules (extra block for better overview)
[mypy-xknx.devices.device,xknx.devices.devices,xknx.devices.travelcalculator,xknx.remote_value.remote_value]
[mypy-xknx.remote_value.remote_value,xknx.remote_value.remote_value_climate_mode]
strict = true
ignore_errors = false
warn_unreachable = true
Expand Down
10 changes: 6 additions & 4 deletions test/config_tests/config_v1_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,9 @@ def test_config_climate_operation_mode(self):
self.assertEqual(
TestConfig.xknx.devices["Office.Climate"].mode,
ClimateMode(
TestConfig.xknx, name=None, group_address_operation_mode="1/7/6"
TestConfig.xknx,
name="Office.Climate_mode",
group_address_operation_mode="1/7/6",
),
)

Expand All @@ -392,7 +394,7 @@ def test_config_climate_operation_mode2(self):
TestConfig.xknx.devices["Attic.Climate"].mode,
ClimateMode(
TestConfig.xknx,
name=None,
name="Attic.Climate_mode",
group_address_operation_mode_protection="1/7/8",
group_address_operation_mode_night="1/7/9",
group_address_operation_mode_comfort="1/7/10",
Expand All @@ -405,7 +407,7 @@ def test_config_climate_operation_mode_state(self):
TestConfig.xknx.devices["Bath.Climate"].mode,
ClimateMode(
TestConfig.xknx,
name=None,
name="Bath.Climate_mode",
group_address_operation_mode="1/7/6",
group_address_operation_mode_state="1/7/7",
),
Expand All @@ -417,7 +419,7 @@ def test_config_climate_controller_status_state(self):
TestConfig.xknx.devices["Cellar.Climate"].mode,
ClimateMode(
TestConfig.xknx,
name=None,
name="Cellar.Climate_mode",
group_address_controller_status="1/7/12",
group_address_controller_status_state="1/7/13",
),
Expand Down
3 changes: 1 addition & 2 deletions test/devices_tests/climate_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@
DPTHVACMode,
DPTTemperature,
DPTValue1Count,
HVACOperationMode,
)
from xknx.dpt.dpt_hvac_mode import HVACControllerMode
from xknx.dpt.dpt_hvac_mode import HVACControllerMode, HVACOperationMode
from xknx.exceptions import CouldNotParseTelegram, DeviceIllegalValue
from xknx.telegram import GroupAddress, Telegram
from xknx.telegram.apci import GroupValueRead, GroupValueWrite
Expand Down
3 changes: 2 additions & 1 deletion test/dpt_tests/dpt_hvac_mode_test.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""Unit test for KNX DPT HVAC Operation modes."""
import unittest

from xknx.dpt import DPTControllerStatus, DPTHVACMode, HVACOperationMode
from xknx.dpt import DPTControllerStatus, DPTHVACMode
from xknx.dpt.dpt_hvac_mode import HVACOperationMode
from xknx.exceptions import ConversionError, CouldNotParseKNXIP


Expand Down
11 changes: 2 additions & 9 deletions test/remote_value_tests/remote_value_climate_mode_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@
import unittest

from xknx import XKNX
from xknx.dpt import DPTArray, DPTBinary, HVACOperationMode
from xknx.dpt.dpt_hvac_mode import HVACControllerMode
from xknx.dpt import DPTArray, DPTBinary
from xknx.dpt.dpt_hvac_mode import HVACControllerMode, HVACOperationMode
from xknx.exceptions import ConversionError, CouldNotParseTelegram
from xknx.remote_value import (
RemoteValueBinaryHeatCool,
RemoteValueBinaryOperationMode,
RemoteValueClimateMode,
)
from xknx.remote_value.remote_value_climate_mode import _RemoteValueBinaryClimateMode
from xknx.telegram import GroupAddress, Telegram
from xknx.telegram.apci import GroupValueWrite

Expand Down Expand Up @@ -137,12 +136,6 @@ def test_from_knx_unknown_operation_mode(self):
with self.assertRaises(ConversionError):
RemoteValueBinaryHeatCool(xknx, controller_mode=None)

def test_supported_operation_modes_not_implemented(self):
"""Test from_knx function with unsupported operation."""
xknx = XKNX()
with self.assertRaises(NotImplementedError):
_RemoteValueBinaryClimateMode.supported_operation_modes()

def test_to_knx_error_operation_mode(self):
"""Test to_knx function with wrong parameter."""
xknx = XKNX()
Expand Down
13 changes: 3 additions & 10 deletions test/remote_value_tests/remote_value_datetime_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,9 @@
import unittest

from xknx import XKNX
from xknx.dpt import DPTArray, DPTBinary, HVACOperationMode
from xknx.exceptions import ConversionError, CouldNotParseTelegram
from xknx.remote_value import (
RemoteValueBinaryHeatCool,
RemoteValueBinaryOperationMode,
RemoteValueClimateMode,
RemoteValueDateTime,
)
from xknx.remote_value.remote_value_climate_mode import _RemoteValueBinaryClimateMode
from xknx.telegram import GroupAddress, Telegram
from xknx.dpt import DPTArray
from xknx.exceptions import ConversionError
from xknx.remote_value import RemoteValueDateTime


class TestRemoteValueDateTime(unittest.TestCase):
Expand Down
10 changes: 0 additions & 10 deletions test/remote_value_tests/remote_value_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,6 @@ def tearDown(self):
"""Tear down test class."""
self.loop.close()

def test_warn_payload_valid(self):
"""Test for warning if payload_valid is not implemented."""
xknx = XKNX()
remote_value = RemoteValue(xknx)
with patch("logging.Logger.warning") as mock_warn:
remote_value.payload_valid(DPTBinary(0))
mock_warn.assert_called_with(
"'payload_valid()' not implemented for %s", "RemoteValue"
)

def test_info_set_uninitialized(self):
"""Test for info if RemoteValue is not initialized."""
xknx = XKNX()
Expand Down
45 changes: 31 additions & 14 deletions xknx/devices/action.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
"""Module for handling commands which may be attached to BinarySensor class."""
import logging
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Optional

if TYPE_CHECKING:
from xknx.xknx import XKNX

logger = logging.getLogger("xknx.log")


class ActionBase:
"""Base Class for handling commands."""

def __init__(self, xknx, hook="on", counter=1):
def __init__(self, xknx: "XKNX", hook: str = "on", counter: Optional[int] = 1):
"""Initialize Action_Base class."""
self.xknx = xknx
self.hook = hook
self.counter = counter

def test_counter(self, counter):
def test_counter(self, counter: Optional[int]) -> bool:
"""Test if action filters for specific counter."""
if self.counter is None:
# no specific counter_filter -> always true
Expand All @@ -22,55 +26,62 @@ def test_counter(self, counter):
return True
return counter == self.counter

def test_if_applicable(self, state, counter=None):
def test_if_applicable(self, state: bool, counter: Optional[int] = None) -> bool:
"""Test if should be executed for this state and this counter number."""
if state and (self.hook == "on"):
return self.test_counter(counter)
if not state and (self.hook == "off"):
return self.test_counter(counter)
return False

async def execute(self):
async def execute(self) -> None:
"""Execute action. To be overwritten in derived classes."""
logger.info("Execute not implemented for %s", self.__class__.__name__)

def __str__(self):
def __str__(self) -> str:
"""Return object as readable string."""
return f'<ActionBase hook="{self.hook}" counter="{self.counter}"/>'

def __eq__(self, other):
def __eq__(self, other: object) -> bool:
"""Equal operator."""
return self.__dict__ == other.__dict__


class Action(ActionBase):
"""Class for handling commands."""

def __init__(self, xknx, hook="on", target=None, method=None, counter=1):
def __init__(
self,
xknx: "XKNX",
hook: str = "on",
target: Optional[str] = None,
method: Optional[str] = None,
counter: int = 1,
):
"""Initialize Action class."""
# pylint: disable=too-many-arguments
super().__init__(xknx, hook, counter)
self.target = target
self.method = method

@classmethod
def from_config(cls, xknx, config):
def from_config(cls, xknx: "XKNX", config: Any) -> "Action":
"""Initialize object from configuration structure."""
hook = config.get("hook", "on")
target = config.get("target")
method = config.get("method")
counter = config.get("counter", 1)
return cls(xknx, hook=hook, target=target, method=method, counter=counter)

async def execute(self):
async def execute(self) -> None:
"""Execute action."""
if self.target is not None:
if self.target is not None and self.method is not None:
if self.target not in self.xknx.devices:
logger.warning("Unknown device %s witin action %s.", self.target, self)
return
await self.xknx.devices[self.target].do(self.method)

def __str__(self):
def __str__(self) -> str:
"""Return object as readable string."""
return '<Action target="{}" method="{}" {}/>'.format(
self.target, self.method, super().__str__()
Expand All @@ -80,17 +91,23 @@ def __str__(self):
class ActionCallback(ActionBase):
"""Class for handling commands via callbacks."""

def __init__(self, xknx, callback, hook="on", counter=1):
def __init__(
self,
xknx: "XKNX",
callback: Callable[[], Awaitable[None]],
hook: str = "on",
counter: int = 1,
):
"""Initialize Action class."""
# pylint: disable=too-many-arguments
super().__init__(xknx, hook, counter)
self.callback = callback

async def execute(self):
async def execute(self) -> None:
"""Execute callback."""
await self.callback()

def __str__(self):
def __str__(self) -> str:
"""Return object as readable string."""
return '<ActionCallback callback="{}" {}/>'.format(
self.callback.__name__, super().__str__()
Expand Down