Reconnecting client: optional keep-alive & timeout

- lirc timeout, to reliably detect disconnect
This commit is contained in:
Martin Bauer 2020-07-13 11:32:38 +00:00
parent feeefbe987
commit 349861ad7c
3 changed files with 66 additions and 31 deletions

View File

@ -2,7 +2,9 @@
import logging import logging
import voluptuous as vol import voluptuous as vol
from datetime import timedelta
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.const import CONF_HOST, CONF_PORT
from ..reconnecting_client import ReconnectingClient from ..reconnecting_client import ReconnectingClient
@ -16,6 +18,8 @@ REPEAT_COUNTER = 'repeat_counter'
LIRC_HOST = 'host' LIRC_HOST = 'host'
DATA_LIRC_NETWORK = 'data_lirc_network' DATA_LIRC_NETWORK = 'data_lirc_network'
LIRC_TIMEOUT = 40 # in seconds
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema(vol.All([{ DOMAIN: vol.Schema(vol.All([{
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
@ -39,7 +43,17 @@ class LircConnection(ReconnectingClient):
def __init__(self, hass, config): def __init__(self, hass, config):
super().__init__(hass, config[CONF_HOST], config[CONF_PORT], "lirc_network", super().__init__(hass, config[CONF_HOST], config[CONF_PORT], "lirc_network",
receive_line_callback=self._process_line, receive_line_callback=self._process_line,
connection_status_changed_callback=self._connection_state_changed) connection_status_changed_callback=self._connection_state_changed,
timeout=LIRC_TIMEOUT)
self._multiline_response_in_progress = False
self._multiline_response = ""
async def keep_alive_callback(*args, **kwargs):
if self._writer and self.connected:
self._writer.write("VERSION\n".encode())
await self._writer.drain()
async_track_time_interval(hass, keep_alive_callback, timedelta(seconds=LIRC_TIMEOUT / 2))
async def _connection_state_changed(self, _): async def _connection_state_changed(self, _):
pass pass
@ -48,17 +62,27 @@ class LircConnection(ReconnectingClient):
# Example msg: # Example msg:
# 0000000000001795 00 Down Hauppauge_350 # 0000000000001795 00 Down Hauppauge_350
# 0000000000001795 01 Down Hauppauge_350 # 0000000000001795 01 Down Hauppauge_350
splitted_line = line.split() if line.startswith("Unknown LIRC Command received: VERSION"):
if len(splitted_line) != 4: pass # response of irserver to VERSION query
_LOGGER.warning(f'Ignoring LIRC message from host {self._host}: "{line}"') elif line.strip() == "BEGIN":
return self._multiline_response_in_progress = True
elif self._multiline_response_in_progress and line.strip() == "END":
self._multiline_response_in_progress = False
self._multiline_response = ""
elif self._multiline_response_in_progress:
self._multiline_response += line
else: else:
code, repeat_counter, key_name, remote_name = splitted_line splitted_line = line.split()
repeat_counter = int(repeat_counter, 16) # repeat code is hexadecimal if len(splitted_line) != 4:
key_name = key_name.lower() _LOGGER.warning(f'Ignoring LIRC message from host {self._host}: "{line}"')
data = {BUTTON_NAME: key_name, return
REMOTE_NAME: remote_name, else:
REPEAT_COUNTER: repeat_counter, code, repeat_counter, key_name, remote_name = splitted_line
LIRC_HOST: self._host} repeat_counter = int(repeat_counter, 16) # repeat code is hexadecimal
_LOGGER.info(f"Got new LIRC network code {data}") key_name = key_name.lower()
self.hass.bus.fire(EVENT_IR_COMMAND_RECEIVED, data) data = {BUTTON_NAME: key_name,
REMOTE_NAME: remote_name,
REPEAT_COUNTER: repeat_counter,
LIRC_HOST: self._host}
_LOGGER.info(f"Got new LIRC network code {data}")
self.hass.bus.fire(EVENT_IR_COMMAND_RECEIVED, data)

View File

@ -2,6 +2,7 @@ import asyncio
import logging import logging
from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
import socket
class IsConnectedSensor(BinarySensorDevice): class IsConnectedSensor(BinarySensorDevice):
@ -12,16 +13,16 @@ class IsConnectedSensor(BinarySensorDevice):
async def set_value(self, value): async def set_value(self, value):
self._on = value self._on = value
await self.async_update_ha_state() # this doesn't work sometimes - but polling is enabled anyway
try:
await asyncio.wait_for(self.async_update_ha_state(), 1)
except Exception:
pass
@property @property
def name(self): def name(self):
return self._name return self._name
@property
def should_poll(self):
return False
@property @property
def available(self) -> bool: def available(self) -> bool:
return True return True
@ -30,13 +31,20 @@ class IsConnectedSensor(BinarySensorDevice):
def is_on(self): def is_on(self):
return self._on return self._on
def refresh(self):
pass
def update(self):
pass
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class ReconnectingClient: class ReconnectingClient:
def __init__(self, hass, host, port, connection_name, receive_line_callback, connection_status_changed_callback): def __init__(self, hass, host, port, connection_name, receive_line_callback,
connection_status_changed_callback, timeout=None):
self.connected = False self.connected = False
self.reconnect_time_start = 1 self.reconnect_time_start = 1
self.reconnect_time_max = 60 self.reconnect_time_max = 60
@ -52,6 +60,7 @@ class ReconnectingClient:
self._connection_name = connection_name self._connection_name = connection_name
self._connection_task = None self._connection_task = None
self._connected_sensor = None self._connected_sensor = None
self._timeout = timeout
@property @property
def connected_sensor(self): def connected_sensor(self):
@ -100,23 +109,25 @@ class ReconnectingClient:
self.reconnect_time = self.reconnect_time_start self.reconnect_time = self.reconnect_time_start
while self._run: while self._run:
line = await reader.readline() if self._timeout:
line = await asyncio.wait_for(reader.readline(), self._timeout)
else:
line = await reader.readline()
if not line: if not line:
raise OSError("Disconnect") raise OSError("Disconnect")
line = line.decode() line = line.decode()
_LOGGER.debug(f"{self._connection_name} received line - passing along '{line}'") _LOGGER.debug(f"{self._connection_name} received line - passing along '{line}'")
await self._receive_line_callback(line) await self._receive_line_callback(line)
except OSError as e: except (OSError, asyncio.TimeoutError):
if self._connection_last_state != 'FAILED': if self._connection_last_state != 'FAILED':
notification_text = "{} connection to {}:{} failed".format(self._connection_name, self._host, notification_text = f"{self._connection_name} connection to {self._host}:{self._port} failed"
self._port)
self.hass.components.persistent_notification.async_create(notification_text, title="No connection") self.hass.components.persistent_notification.async_create(notification_text, title="No connection")
_LOGGER.error("Connection to {} failed {}:{}".format(self._connection_name, self._host, self._port)) _LOGGER.error(f"Connection to {self._connection_name} failed {self._host}:{self._port}")
await self._connection_status_changed_callback('disconnected') await self._connection_status_changed_callback('disconnected')
if self._connected_sensor: if self._connected_sensor:
await self._connected_sensor.set_value(False) await asyncio.wait_for(self._connected_sensor.set_value(False), timeout=2.0)
_LOGGER.error( _LOGGER.error(f"After setting sensor ({self._connection_name} {self._host}:{self._port})")
"After connection failed ({} {}:{})".format(self._connection_name, self._host, self._port)) _LOGGER.error(f"After connection failed ({self._connection_name} {self._host}:{self._port})")
else: else:
_LOGGER.debug(f"{self._connection_name} retried connection, last state {self._connection_last_state}" _LOGGER.debug(f"{self._connection_name} retried connection, last state {self._connection_last_state}"
f"reconnection time {self.reconnect_time}") f"reconnection time {self.reconnect_time}")

View File

@ -441,9 +441,6 @@ class SqueezeBoxDevice(MediaPlayerDevice):
"""Flag media player features that are supported.""" """Flag media player features that are supported."""
return SUPPORT_SQUEEZEBOX return SUPPORT_SQUEEZEBOX
def turn_off(self):
self.call_method('power', '0')
def volume_up(self): def volume_up(self):
self.call_method('mixer', 'volume', '+5') self.call_method('mixer', 'volume', '+5')
@ -479,6 +476,9 @@ class SqueezeBoxDevice(MediaPlayerDevice):
def turn_on(self): def turn_on(self):
self.call_method('power', '1') self.call_method('power', '1')
def turn_off(self):
self.call_method('power', '0')
def play_media(self, media_type, media_id, **kwargs): def play_media(self, media_type, media_id, **kwargs):
""" """
Send the play_media command to the media player. Send the play_media command to the media player.