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

Tellstick Duo acync callback fix #10384

Merged
merged 19 commits into from
Nov 9, 2017
16 changes: 7 additions & 9 deletions homeassistant/components/light/tellstick.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,13 @@
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.tellstick/
"""
import voluptuous as vol

from homeassistant.components.light import (
ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light)
from homeassistant.components.tellstick import (
DEFAULT_SIGNAL_REPETITIONS, ATTR_DISCOVER_DEVICES, ATTR_DISCOVER_CONFIG,
DOMAIN, TellstickDevice)
DATA_TELLSTICK, TellstickDevice)

PLATFORM_SCHEMA = vol.Schema({vol.Required("platform"): DOMAIN})

SUPPORT_TELLSTICK = SUPPORT_BRIGHTNESS

Expand All @@ -27,9 +25,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
signal_repetitions = discovery_info.get(
ATTR_DISCOVER_CONFIG, DEFAULT_SIGNAL_REPETITIONS)

add_devices(TellstickLight(tellcore_id, hass.data['tellcore_registry'],
signal_repetitions)
for tellcore_id in discovery_info[ATTR_DISCOVER_DEVICES])
add_devices([TellstickLight(tellcore_id, hass.data[DATA_TELLSTICK],
signal_repetitions)
for tellcore_id in discovery_info[ATTR_DISCOVER_DEVICES]],
True)


class TellstickLight(TellstickDevice, Light):
Expand Down Expand Up @@ -57,9 +56,8 @@ def _parse_ha_data(self, kwargs):

def _parse_tellcore_data(self, tellcore_data):
"""Turn the value received from tellcore into something useful."""
if tellcore_data is not None:
brightness = int(tellcore_data)
return brightness
if tellcore_data:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works, but the fix is conceptually in the wrong place. When we get into _update_model_from_command, we know the tellcore_command, and we know that it's TELLSTICK_DIM. That's the only use-case where parsing the data makes sense, per the docs:

If method is TELLSTICK_DIM this holds the current value as a human readable string, example "128" for 50%.

The fix should be in _update_model_from_command, never calling _parse_tellcore_data, which by the time is called doesn't know what the command was, and can't reason about whether it's a failure that we have no data or if it's due to receiving one of the commands that have no data in the first place.

return int(tellcore_data) # brightness
return None

def _update_model(self, new_state, data):
Expand Down
22 changes: 9 additions & 13 deletions homeassistant/components/switch/tellstick.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,11 @@
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/switch.tellstick/
"""
import voluptuous as vol

from homeassistant.components.tellstick import (DEFAULT_SIGNAL_REPETITIONS,
ATTR_DISCOVER_DEVICES,
ATTR_DISCOVER_CONFIG,
DOMAIN, TellstickDevice)
from homeassistant.components.tellstick import (
DEFAULT_SIGNAL_REPETITIONS, ATTR_DISCOVER_DEVICES,
ATTR_DISCOVER_CONFIG, DATA_TELLSTICK, TellstickDevice)
from homeassistant.helpers.entity import ToggleEntity

PLATFORM_SCHEMA = vol.Schema({vol.Required("platform"): DOMAIN})


# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
Expand All @@ -26,21 +21,22 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
signal_repetitions = discovery_info.get(ATTR_DISCOVER_CONFIG,
DEFAULT_SIGNAL_REPETITIONS)

add_devices(TellstickSwitch(tellcore_id, hass.data['tellcore_registry'],
signal_repetitions)
for tellcore_id in discovery_info[ATTR_DISCOVER_DEVICES])
add_devices([TellstickSwitch(tellcore_id, hass.data[DATA_TELLSTICK],
signal_repetitions)
for tellcore_id in discovery_info[ATTR_DISCOVER_DEVICES]],
True)


class TellstickSwitch(TellstickDevice, ToggleEntity):
"""Representation of a Tellstick switch."""

def _parse_ha_data(self, kwargs):
"""Turn the value from HA into something useful."""
return None
pass

def _parse_tellcore_data(self, tellcore_data):
"""Turn the value received from tellcore into something useful."""
return None
pass

def _update_model(self, new_state, data):
"""Update the device entity state to match the arguments."""
Expand Down
44 changes: 29 additions & 15 deletions homeassistant/components/tellstick.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/tellstick/
"""
import asyncio
import logging
import threading

import voluptuous as vol

from homeassistant.helpers import discovery
from homeassistant.core import callback
from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP, CONF_HOST, CONF_PORT)
from homeassistant.helpers.entity import Entity
Expand All @@ -26,6 +28,8 @@
DEFAULT_SIGNAL_REPETITIONS = 1
DOMAIN = 'tellstick'

DATA_TELLSTICK = 'tellcore_registry'

# Use a global tellstick domain lock to avoid getting Tellcore errors when
# calling concurrently.
TELLSTICK_LOCK = threading.RLock()
Expand Down Expand Up @@ -62,7 +66,7 @@ def _discover(hass, config, component_name, found_tellcore_devices):
def setup(hass, config):
"""Set up the Tellstick component."""
from tellcore.constants import TELLSTICK_DIM
from tellcore.telldus import QueuedCallbackDispatcher
from tellcore.telldus import AsyncioCallbackDispatcher
from tellcore.telldus import TelldusCore
from tellcorenet import TellCoreClient

Expand All @@ -83,7 +87,7 @@ def stop_tellcore_net(event):

try:
tellcore_lib = TelldusCore(
callback_dispatcher=QueuedCallbackDispatcher())
callback_dispatcher=AsyncioCallbackDispatcher(hass.loop))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is, you need implement all in async to do that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, can you expand on that? Would that be cleaner still than flushing the queue of the QueuedCallbackDispatcher regularly (as I understood is needed to make callbacks happen for QueuedCallbackDispatcher)? Hoping we can find some fix to this, #10329 is a pretty serious bug for my HA-usecase.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This platform/components run in our threaded environment. So it need run inside threads. If you want run asynchronous, you need port this into our async interfaces

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know the Home Assistant codebase too well, so it's not clear to me what that would require. @stefan-jonasson, ideas? @pvizeli, since you know the code well, and landed 1c8f179 which regressed this, perhaps you could land a fix for it too? Would love to get my switches working again 😃

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pvizeli Can you describe the actual problem we trying to avoid (because the previous implementation worked just fine)? When you say all exactly what does that entail, just the callback handling or do we need to rewrite the whole platform? If so could you point me to some examples from other platforms that implemented in that way?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pvizeli Are you saying we can or can't revert this? If we can't, are you saying there will be a fix for 0.57.3?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@torarnv Until this has been resolved, you can always just modify your local tellstick.py file and revert the breaking changes yourself. I did that and my HA runs as smoothly as ever.

Sure, there might be problems with blocking the core loop and whatnot, but I haven't experienced any issues yet.

Copy link
Member

@pvizeli pvizeli Nov 8, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stefan-jonasson we should discuss a real solution here and no flameware. If you convert the callback they will be called from tellcore library to async (tag it with @callback from core.py too) it should work. Maybe that is the fast way and maybe also the best way.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I give a try, but I can't test it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pvizeli Happy to test anything you come up with! 😄

except OSError:
_LOGGER.exception("Could not initialize Tellstick")
return False
Expand All @@ -94,7 +98,7 @@ def stop_tellcore_net(event):
# Register devices
tellcore_registry = TellstickRegistry(hass, tellcore_lib)
tellcore_registry.register_tellcore_devices(all_tellcore_devices)
hass.data['tellcore_registry'] = tellcore_registry
hass.data[DATA_TELLSTICK] = tellcore_registry

# Discover the switches
_discover(hass, config, 'switch',
Expand Down Expand Up @@ -124,30 +128,35 @@ class TellstickRegistry(object):
def __init__(self, hass, tellcore_lib):
"""Initialize the Tellstick mappings and callbacks."""
# used when map callback device id to ha entities.
self.hass = hass
self._id_to_ha_device_map = {}
self._id_to_tellcore_device_map = {}
self._setup_tellcore_callback(hass, tellcore_lib)
self._setup_tellcore_callback(tellcore_lib)

def _tellcore_event_callback(self, tellcore_id, tellcore_command,
tellcore_data, cid):
@callback
def _async_tellcore_event_callback(self, tellcore_id, tellcore_command,
tellcore_data, cid):
"""Handle the actual callback from Tellcore."""
ha_device = self._id_to_ha_device_map.get(tellcore_id, None)
if ha_device is not None:
# Pass it on to the HA device object
ha_device.update_from_callback(tellcore_command, tellcore_data)
self.hass.async_add_job(
ha_device.update_from_callback, tellcore_command,
tellcore_data)

def _setup_tellcore_callback(self, hass, tellcore_lib):
def _setup_tellcore_callback(self, tellcore_lib):
"""Register the callback handler."""
callback_id = tellcore_lib.register_device_event(
self._tellcore_event_callback)
self._async_tellcore_event_callback)

def clean_up_callback(event):
"""Unregister the callback bindings."""
if callback_id is not None:
tellcore_lib.unregister_callback(callback_id)
_LOGGER.debug("Tellstick callback unregistered")

hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, clean_up_callback)
self.hass.bus.listen_once(
EVENT_HOMEASSISTANT_STOP, clean_up_callback)

def register_ha_device(self, tellcore_id, ha_device):
"""Register a new HA device to receive callback updates."""
Expand Down Expand Up @@ -182,10 +191,16 @@ def __init__(self, tellcore_id, tellcore_registry, signal_repetitions):
self._tellcore_device = tellcore_registry.get_tellcore_device(
tellcore_id)
self._name = self._tellcore_device.name
# Query tellcore for the current state
self._update_from_tellcore()

@asyncio.coroutine
def async_added_to_hass(self):
"""Register callbacks."""
tellcore_registry = self.hass.data[DATA_TELLSTICK]

# Add ourselves to the mapping for callbacks
tellcore_registry.register_ha_device(tellcore_id, self)
self.hass.async_add_job(
tellcore_registry.register_ha_device,
self._tellcore_device.id, self)

@property
def should_poll(self):
Expand Down Expand Up @@ -283,7 +298,7 @@ def update_from_callback(self, tellcore_command, tellcore_data):
# This is a benign race on _repeats_left -- it's checked with the lock
# in _send_repeated_command.
if self._repeats_left > 0:
self.hass.async_add_job(self._send_repeated_command)
self.hass.add_job(self._send_repeated_command)

def _update_from_tellcore(self):
"""Read the current state of the device from the tellcore library."""
Expand All @@ -303,4 +318,3 @@ def _update_from_tellcore(self):
def update(self):
"""Poll the current state of the device."""
self._update_from_tellcore()
self.schedule_update_ha_state()