Repository: XKNX/xknx
Pull request #528 (2020-12-13 00:26:00)

type-check Core
<!--
  You are awesome! Thanks for contributing to our project!
  Please, DO NOT DELETE ANY TEXT from this template!.
-->
## Description
<!--
Please include a summary of the change and which issue is fixed.
Please also include relevant motivation and context.
List any dependencies that are required for this change.
-->
changes: 
- StateUpdater allow float values for `register_remote_value(tracker_options)` attribute
  because why not

Fixes # (issue)

## Type of change
<!--
Please tick the applicable options.
NOTE: Ticking multiple options most likely indicates
that your change is to big and it is suggested to split it into several smaller PRs.
-->

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update

## Checklist:

- [x] I have performed a self-review of my own code
- [ ] The documentation has been adjusted accordingly
- [x] The changes generate no new warnings
- [ ] Tests have been added that prove the fix is effective or that the feature works
- [x] The changes are documented in the changelog
- [ ] The Homeassistant plugin has been adjusted in case of new config options


coveralls: 
[![Coverage Status](https://coveralls.io/builds/35667661/badge)](https://coveralls.io/builds/35667661)

Coverage increased (+0.003%) to 94.668% when pulling **f84498f12716939613f772d91756cb8f4de5730a on farmio:core-type-check** into **5957eb089866f6be463353c73e0855d6da05e18e on XKNX:master**.


Patch:
diff --git a/changelog.md b/changelog.md
index ebaf212b..8d91c307 100644
--- a/changelog.md
+++ b/changelog.md
@@ -18,6 +18,7 @@
 - Telegram: remove `Telegram.telegramtype` and replace with payload object derived from `xknx.telegram.apci.APCI`.
 - CEMIFrame: remove `CEMIFrame.cmd`, which can be derived from `CEMIFrame.payload`.
 - Farewell Travis CI; Welcome Github Actions!
+- StateUpdater allow float values for `register_remote_value(tracker_options)` attribute.
 
 ## 0.15.6 Bugfix for StateUpater 2020-11-26
 
diff --git a/setup.cfg b/setup.cfg
index dfed14ef..12ac9c72 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -42,7 +42,7 @@ warn_redundant_casts = true
 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
-[mypy-xknx.core.value_reader,xknx.devices.travelcalculator,xknx.exceptions.*,xknx.io.gateway_scanner,xknx.telegram.*,]
+[mypy-xknx.core.*,xknx.devices.travelcalculator,xknx.exceptions.*,xknx.io.gateway_scanner,xknx.telegram.*,]
 strict = true
 ignore_errors = false
 warn_unreachable = true
diff --git a/xknx/core/state_updater.py b/xknx/core/state_updater.py
index d5b6bad7..0aeaebf6 100644
--- a/xknx/core/state_updater.py
+++ b/xknx/core/state_updater.py
@@ -2,37 +2,48 @@
 import asyncio
 from enum import Enum
 import logging
+from typing import TYPE_CHECKING, Awaitable, Callable, Dict, Optional, Tuple, Union
 
 from xknx.remote_value import RemoteValue
 
-DEFAULT_UPDATE_INTERVAL = 60
-MAX_UPDATE_INTERVAL = 1440
+if TYPE_CHECKING:
+    from xknx.xknx import XKNX
+
 
 logger = logging.getLogger("xknx.state_updater")
 
+DEFAULT_UPDATE_INTERVAL = 60
+MAX_UPDATE_INTERVAL = 1440
+
 
 class StateUpdater:
     """Class for keeping the states of RemoteValues up to date."""
 
-    def __init__(self, xknx, parallel_reads: int = 2):
+    def __init__(self, xknx: "XKNX", parallel_reads: int = 2):
         """Initialize StateUpdater class."""
         self.xknx = xknx
         self.started = False
-        self._workers = {}
+        self._workers: Dict[int, _StateTracker] = {}
         self._semaphore = asyncio.Semaphore(value=parallel_reads)
 
-    def register_remote_value(self, remote_value: RemoteValue, tracker_options=True):
+    def register_remote_value(
+        self,
+        remote_value: RemoteValue,
+        tracker_options: Union[bool, int, float, str] = True,
+    ) -> None:
         """Register a RemoteValue to initialize its state and/or track for expiration."""
 
-        def parse_tracker_options(tracker_options):
+        def parse_tracker_options(
+            tracker_options: Union[bool, int, float, str]
+        ) -> Tuple[StateTrackerType, Union[int, float]]:
             """Parse tracker type and expiration time."""
             tracker_type = StateTrackerType.EXPIRE
-            update_interval = DEFAULT_UPDATE_INTERVAL
+            update_interval: Union[int, float] = DEFAULT_UPDATE_INTERVAL
 
             if isinstance(tracker_options, bool):
                 # `True` would be overwritten by the check for `int`
                 return (tracker_type, update_interval)
-            if isinstance(tracker_options, int):
+            if isinstance(tracker_options, (int, float)):
                 update_interval = tracker_options
             elif isinstance(tracker_options, str):
                 _options = tracker_options.split()
@@ -56,15 +67,7 @@ class StateUpdater:
                         update_interval = int(_options[1])
                 except IndexError:
                     pass  # No time given (no _options[1])
-            else:
-                logger.warning(
-                    'Could not parse StateUpdater tracker_options type %s "%s" for %s. Using default %s %s minutes.',
-                    type(tracker_options),
-                    tracker_options,
-                    remote_value,
-                    tracker_type,
-                    update_interval,
-                )
+
             if update_interval > MAX_UPDATE_INTERVAL:
                 logger.warning(
                     "StateUpdater interval of %s to long for %s. Using maximum of %s minutes (1 day)",
@@ -75,7 +78,7 @@ class StateUpdater:
                 update_interval = MAX_UPDATE_INTERVAL
             return (tracker_type, update_interval)
 
-        async def read_state_mutex():
+        async def read_state_mutex() -> None:
             """Schedule to read the state from the KNX bus - one at a time."""
             async with self._semaphore:
                 # wait until there is nothing else to send to the bus
@@ -92,9 +95,9 @@ class StateUpdater:
 
         tracker_type, update_interval = parse_tracker_options(tracker_options)
         tracker = _StateTracker(
+            read_state_awaitable=read_state_mutex,
             tracker_type=tracker_type,
             interval_min=update_interval,
-            read_state_awaitable=read_state_mutex,
         )
         self._workers[id(remote_value)] = tracker
 
@@ -107,23 +110,23 @@ class StateUpdater:
         if self.started:
             tracker.start()
 
-    def unregister_remote_value(self, remote_value: RemoteValue):
+    def unregister_remote_value(self, remote_value: RemoteValue) -> None:
         """Unregister a RemoteValue from StateUpdater."""
         self._workers.pop(id(remote_value)).stop()
 
-    def update_received(self, remote_value: RemoteValue):
+    def update_received(self, remote_value: RemoteValue) -> None:
         """Reset the timer when a state update was received."""
         if self.started and id(remote_value) in self._workers:
             self._workers[id(remote_value)].update_received()
 
-    def start(self):
+    def start(self) -> None:
         """Start StateUpdater. Initialize states."""
         logger.debug("StateUpdater initializing values")
         self.started = True
         for worker in self._workers.values():
             worker.start()
 
-    def stop(self):
+    def stop(self) -> None:
         """Stop StateUpdater."""
         logger.debug("StateUpdater stopping")
         self.started = False
@@ -145,45 +148,45 @@ class _StateTracker:
     # pylint: disable=too-many-instance-attributes
     def __init__(
         self,
+        read_state_awaitable: Callable[[], Awaitable[None]],
         tracker_type: StateTrackerType = StateTrackerType.EXPIRE,
-        interval_min: int = 60,
-        read_state_awaitable=None,
+        interval_min: float = 60,
     ):
         """Initialize StateTracker class."""
         # pylint: disable=too-many-arguments
         self.tracker_type = tracker_type
         self.update_interval = interval_min * 60
         self._read_state = read_state_awaitable
-        self._task = None
+        self._task: Optional[asyncio.Task] = None
 
-    def start(self):
+    def start(self) -> None:
         """Start StateTracker - read state on call."""
         self.stop()
         self._task = asyncio.create_task(self._start_init())
 
-    async def _start_init(self):
+    async def _start_init(self) -> None:
         """Initialize state, start update loop if appropriate."""
         await self._read_state()
         if self.tracker_type is not StateTrackerType.INIT:
             self.reset()
 
-    def reset(self):
+    def reset(self) -> None:
         """Start / Restart StateTracker timer - wait for value to expire."""
         self.stop()
         self._task = asyncio.create_task(self._update_loop())
 
-    def stop(self):
+    def stop(self) -> None:
         """Stop StateTracker."""
         if self._task:
             self._task.cancel()
             self._task = None
 
-    def update_received(self):
+    def update_received(self) -> None:
         """Reset the timer if a telegram was received for a "expire" typed StateUpdater."""
         if self.tracker_type == StateTrackerType.EXPIRE:
             self.reset()
 
-    async def _update_loop(self):
+    async def _update_loop(self) -> None:
         """Wait for the update_interval to expire. Endless loop for updating states."""
         # for StateUpdaterType.EXPIRE:
         #   on successfull read the while loop gets canceled when the callback calls update_received()
diff --git a/xknx/core/telegram_queue.py b/xknx/core/telegram_queue.py
index 5981bdd4..c246ea16 100644
--- a/xknx/core/telegram_queue.py
+++ b/xknx/core/telegram_queue.py
@@ -9,11 +9,17 @@ You may register callbacks to be notified if a telegram was pushed to the queue.
 """
 import asyncio
 import logging
+from typing import TYPE_CHECKING, Awaitable, Callable, List, Optional
 
 from xknx.exceptions import CommunicationError, XKNXException
-from xknx.telegram import TelegramDirection
+from xknx.telegram import AddressFilter, GroupAddress, Telegram, TelegramDirection
 from xknx.telegram.apci import GroupValueWrite
 
+if TYPE_CHECKING:
+    from xknx.xknx import XKNX
+
+    AsyncTelegramCallback = Callable[[Telegram], Awaitable[None]]
+
 logger = logging.getLogger("xknx.log")
 telegram_logger = logging.getLogger("xknx.telegram")
 
@@ -26,51 +32,62 @@ class TelegramQueue:
 
         # pylint: disable=too-few-public-methods
 
-        def __init__(self, callback, address_filters=None):
+        def __init__(
+            self,
+            callback: "AsyncTelegramCallback",
+            address_filters: Optional[List[AddressFilter]] = None,
+        ):
             """Initialize Callback class."""
             self.callback = callback
             self.address_filters = address_filters
 
-        def is_within_filter(self, telegram):
+        def is_within_filter(self, telegram: Telegram) -> bool:
             """Test if callback is filtering for group address."""
             if self.address_filters is None:
                 return True
             for address_filter in self.address_filters:
-                if address_filter.match(telegram.destination_address):
+                if isinstance(
+                    telegram.destination_address, GroupAddress
+                ) and address_filter.match(telegram.destination_address):
                     return True
             return False
 
-    def __init__(self, xknx):
+    def __init__(self, xknx: "XKNX"):
         """Initialize TelegramQueue class."""
         self.xknx = xknx
-        self.telegram_received_cbs = []
-        self.outgoing_queue = asyncio.Queue()
-        self._consumer_task = None
-
-    def register_telegram_received_cb(self, telegram_received_cb, address_filters=None):
+        self.telegram_received_cbs: List[TelegramQueue.Callback] = []
+        self.outgoing_queue: asyncio.Queue[Optional[Telegram]] = asyncio.Queue()
+        self._consumer_task: Optional[Awaitable] = None
+
+    def register_telegram_received_cb(
+        self,
+        telegram_received_cb: "AsyncTelegramCallback",
+        address_filters: Optional[List[AddressFilter]] = None,
+    ) -> Callback:
         """Register callback for a telegram beeing received from KNX bus."""
         callback = TelegramQueue.Callback(telegram_received_cb, address_filters)
         self.telegram_received_cbs.append(callback)
         return callback
 
-    def unregister_telegram_received_cb(self, telegram_received_cb):
+    def unregister_telegram_received_cb(self, telegram_received_cb: Callback) -> None:
         """Unregister callback for a telegram beeing received from KNX bus."""
         self.telegram_received_cbs.remove(telegram_received_cb)
 
-    async def start(self):
+    async def start(self) -> None:
         """Start telegram queue."""
         self._consumer_task = asyncio.gather(
             self._telegram_consumer(), self._outgoing_rate_limiter()
         )
 
-    async def stop(self):
+    async def stop(self) -> None:
         """Stop telegram queue."""
         logger.debug("Stopping TelegramQueue")
         # If a None object is pushed to the queue, the queue stops
         await self.xknx.telegrams.put(None)
-        await self._consumer_task
+        if self._consumer_task is not None:
+            await self._consumer_task
 
-    async def _telegram_consumer(self):
+    async def _telegram_consumer(self) -> None:
         """Endless loop for processing telegrams."""
         while True:
             telegram = await self.xknx.telegrams.get()
@@ -91,7 +108,7 @@ class TelegramQueue:
             except XKNXException as ex:
                 logger.error("Error while processing telegram %s", ex)
 
-    async def _outgoing_rate_limiter(self):
+    async def _outgoing_rate_limiter(self) -> None:
         """Endless loop for processing outgoing telegrams."""
         while True:
             telegram = await self.outgoing_queue.get()
@@ -115,7 +132,7 @@ class TelegramQueue:
             if self.xknx.rate_limit:
                 await asyncio.sleep(1 / self.xknx.rate_limit)
 
-    async def _process_all_telegrams(self):
+    async def _process_all_telegrams(self) -> None:
         """Process all telegrams being queued. Used in unit tests."""
         while not self.xknx.telegrams.empty():
             try:
@@ -129,7 +146,7 @@ class TelegramQueue:
             finally:
                 self.xknx.telegrams.task_done()
 
-    async def process_telegram_outgoing(self, telegram):
+    async def process_telegram_outgoing(self, telegram: Telegram) -> None:
         """Process outgoing telegram."""
         telegram_logger.debug(telegram)
         if self.xknx.knxip_interface is not None:
@@ -139,7 +156,7 @@ class TelegramQueue:
         else:
             logger.warning("No KNXIP interface defined")
 
-    async def process_telegram_incoming(self, telegram):
+    async def process_telegram_incoming(self, telegram: Telegram) -> None:
         """Process incoming telegram."""
         telegram_logger.debug(telegram)
         for telegram_received_cb in self.telegram_received_cbs:
diff --git a/xknx/remote_value/__init__.py b/xknx/remote_value/__init__.py
index a7d72b8f..aaace6a6 100644
--- a/xknx/remote_value/__init__.py
+++ b/xknx/remote_value/__init__.py
@@ -22,3 +22,26 @@ from .remote_value_string import RemoteValueString
 from .remote_value_switch import RemoteValueSwitch
 from .remote_value_temp import RemoteValueTemp
 from .remote_value_updown import RemoteValueUpDown
+
+__all__ = [
+    "RemoteValue",
+    "RemoteValue1Count",
+    "RemoteValueBinaryHeatCool",
+    "RemoteValueBinaryOperationMode",
+    "RemoteValueClimateMode",
+    "RemoteValueColorRGB",
+    "RemoteValueColorRGBW",
+    "RemoteValueControl",
+    "RemoteValueDateTime",
+    "RemoteValueDpt2ByteUnsigned",
+    "RemoteValueDptValue1Ucount",
+    "RemoteValueScaling",
+    "RemoteValueSceneNumber",
+    "RemoteValueSensor",
+    "RemoteValueSetpointShift",
+    "RemoteValueStep",
+    "RemoteValueString",
+    "RemoteValueSwitch",
+    "RemoteValueTemp",
+    "RemoteValueUpDown",
+]
diff --git a/xknx/remote_value/remote_value.py b/xknx/remote_value/remote_value.py
index dc7a298c..6932e9f6 100644
--- a/xknx/remote_value/remote_value.py
+++ b/xknx/remote_value/remote_value.py
@@ -193,7 +193,7 @@ class RemoteValue:
         if self.payload is not None:
             await self._send(self.payload, response=True)
 
-    async def read_state(self, wait_for_result=False):
+    async def read_state(self, wait_for_result: bool = False) -> None:
         """Send GroupValueRead telegram for state address to KNX bus."""
         if self.readable:
             # pylint: disable=import-outside-toplevel


Test patch:
diff --git a/test/core_tests/state_updater_test.py b/test/core_tests/state_updater_test.py
index 67220a39..4f5ac83a 100644
--- a/test/core_tests/state_updater_test.py
+++ b/test/core_tests/state_updater_test.py
@@ -68,6 +68,20 @@ class TestStateUpdater(unittest.TestCase):
         )
         self.assertEqual(_get_only_tracker().tracker_type, StateTrackerType.INIT)
         remote_value_init.__del__()
+        # DEFAULT with int
+        remote_value_expire = RemoteValue(
+            xknx, sync_state=2, group_address_state=GroupAddress("1/1/1")
+        )
+        self.assertEqual(_get_only_tracker().tracker_type, StateTrackerType.EXPIRE)
+        self.assertEqual(_get_only_tracker().update_interval, 2 * 60)
+        remote_value_expire.__del__()
+        # DEFAULT with float
+        remote_value_expire = RemoteValue(
+            xknx, sync_state=6.9, group_address_state=GroupAddress("1/1/1")
+        )
+        self.assertEqual(_get_only_tracker().tracker_type, StateTrackerType.EXPIRE)
+        self.assertEqual(_get_only_tracker().update_interval, 6.9 * 60)
+        remote_value_expire.__del__()
         # EXPIRE with default time
         remote_value_expire = RemoteValue(
             xknx, sync_state="expire", group_address_state=GroupAddress("1/1/1")
@@ -127,22 +141,6 @@ class TestStateUpdater(unittest.TestCase):
         self.assertEqual(_get_only_tracker().update_interval, 60 * 60)
         remote_value_invalid.__del__()
         logging_warning_mock.reset_mock()
-        # INVALID type
-        remote_value_float = RemoteValue(
-            xknx, sync_state=5.6, group_address_state=GroupAddress("1/1/1")
-        )
-        logging_warning_mock.assert_called_once_with(
-            'Could not parse StateUpdater tracker_options type %s "%s" for %s. Using default %s %s minutes.',
-            float,
-            5.6,
-            remote_value_float,
-            StateTrackerType.EXPIRE,
-            60,
-        )
-        self.assertEqual(_get_only_tracker().tracker_type, StateTrackerType.EXPIRE)
-        self.assertEqual(_get_only_tracker().update_interval, 60 * 60)
-        remote_value_float.__del__()
-        logging_warning_mock.reset_mock()
         # intervall too long
         remote_value_long = RemoteValue(
             xknx, sync_state=1441, group_address_state=GroupAddress("1/1/1")