Factored Client code out & implemented LIRC network listener

This commit is contained in:
Martin Bauer 2019-06-19 19:22:06 +02:00
parent 20f43f4294
commit 1a746d1438
9 changed files with 322 additions and 70 deletions

View File

@ -31,17 +31,13 @@ dimmer:
# Weather prediction # Weather prediction
# - platform: yr # - platform: yr
# Text to speech
#tts:
## - platform: google_translate
# language: 'de'
# base_url: http://192.168.178.73:8123
tts: tts:
- platform: watson_tts - platform: watson_tts
watson_apikey: X_tnnoaZGOwxZlqUn07wkD2G-0vaaAuOw6I6d_6jpCf7 watson_apikey: X_tnnoaZGOwxZlqUn07wkD2G-0vaaAuOw6I6d_6jpCf7
watson_url: https://gateway-lon.watsonplatform.net/text-to-speech/api watson_url: https://gateway-lon.watsonplatform.net/text-to-speech/api
voice: de-DE_BirgitVoice voice: de-DE_BirgitVoice
output_format: audio/flac;rate=44100 output_format: audio/flac
output_audio_rate: 44100
knx: knx:
rate_limit: 20 rate_limit: 20
@ -57,6 +53,13 @@ fhem:
cul_device_name: CUL_HM cul_device_name: CUL_HM
lirc_network:
- host: kitchenpi
port: 2222
- host: bedroompi
port: 2222
media_player: media_player:
- platform: squeezebox - platform: squeezebox
host: server host: server
@ -66,7 +69,6 @@ media_player:
group: !include groups.yaml group: !include groups.yaml
automation: !include automations.yaml automation: !include automations.yaml
script: !include scripts.yaml script: !include scripts.yaml

View File

@ -2,10 +2,10 @@
import logging import logging
import voluptuous as vol import voluptuous as vol
import asyncio
from collections import defaultdict from collections import defaultdict
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP from homeassistant.const import CONF_HOST, CONF_PORT
from ..reconnecting_client import ReconnectingClient
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_CUL_DEVICE_NAME = 'cul_device_name' CONF_CUL_DEVICE_NAME = 'cul_device_name'
@ -23,28 +23,20 @@ CONFIG_SCHEMA = vol.Schema({
async def async_setup(hass, config): async def async_setup(hass, config):
connection = FhemConnection(hass, config) connection = FhemConnection(hass, config[DOMAIN])
hass.data[DATA_FHEM] = connection hass.data[DATA_FHEM] = connection
await connection.start() await connection.start()
return True return True
class FhemConnection: class FhemConnection(ReconnectingClient):
def __init__(self, hass, config): def __init__(self, hass, config):
self.hass = hass super().__init__(hass, config[CONF_HOST], config[CONF_PORT], "FHEM",
self._host = config[DOMAIN][CONF_HOST] receive_line_callback=self._process_line,
self._port = config[DOMAIN][CONF_PORT] connection_status_changed_callback=self._update_all_devices)
self._cul_device_name = config[DOMAIN][CONF_CUL_DEVICE_NAME] self._cul_device_name = config[CONF_CUL_DEVICE_NAME]
self.connected = False
self.reconnect_time_start = 1
self.reconnect_time_max = 60
self.reconnect_time = self.reconnect_time_start
self._devices = defaultdict(list) self._devices = defaultdict(list)
self._run = False
self._writer = None
self._connection_last_state = 'UNKNOWN'
def register_device(self, id, d): def register_device(self, id, d):
self._devices[id].append(d) self._devices[id].append(d)
@ -53,56 +45,11 @@ class FhemConnection:
"displayattr .*\n".encode(), "displayattr .*\n".encode(),
]) ])
async def _update_all_devices(self): async def _update_all_devices(self, _):
for device_list in self._devices.values(): for device_list in self._devices.values():
for device in device_list: for device in device_list:
await device.async_update_ha_state() await device.async_update_ha_state()
async def start(self):
self._run = True
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.stop)
self.hass.loop.create_task(self._connection())
async def stop(self):
self._run = False
self.connected = False
async def _connection(self):
try:
reader, writer = await asyncio.open_connection(self._host, self._port)
_LOGGER.info("Connected to FHEM {}:{}".format(self._host, self._port))
self._connection_last_state = 'CONNECTED'
self._writer = writer
self.connected = True
await self._update_all_devices()
self.reconnect_time = self.reconnect_time_start
writer.writelines([
"displayattr .*\n".encode(),
"inform on\n".encode(),
])
while self._run:
line = await reader.readline()
if not line:
_LOGGER.warning("FHEM disconnected: {}".format(line))
raise OSError("Disconnect")
line = line.decode()
_LOGGER.debug("FHEM received line: {}".format(line))
await self._process_line(line)
except OSError:
if self._connection_last_state != 'FAILED':
self.hass.components.persistent_notification.async_create("FHEM connection failed",
title="No FHEM connection")
_LOGGER.error("Connection to FHEM failed {}:{}".format(self._host, self._port))
self._connection_last_state = 'FAILED'
self.connected = False
await self._update_all_devices()
await asyncio.sleep(self.reconnect_time)
self.reconnect_time = min(2 * self.reconnect_time, self.reconnect_time_max)
self.hass.loop.create_task(self._connection())
async def _process_line(self, line): async def _process_line(self, line):
if line.startswith(self._cul_device_name + " "): # Status update message if line.startswith(self._cul_device_name + " "): # Status update message
_, device_name, command = line.split(" ", 2) _, device_name, command = line.split(" ", 2)
@ -127,7 +74,7 @@ class FhemConnection:
:param arguments: string or list of strings containing command parameters :param arguments: string or list of strings containing command parameters
""" """
arguments = " ".join([str(a) for a in arguments]) arguments = " ".join([str(a) for a in arguments])
self._writer.write("set {} {}\n".format(id, arguments).encode()) self.write_line("set {} {}\n".format(id, arguments))
def device_error_reporting(hass, received_line, component_type, component_name): def device_error_reporting(hass, received_line, component_type, component_name):

View File

@ -0,0 +1,59 @@
"""Integration of lirc network service"""
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.lirc import EVENT_IR_COMMAND_RECEIVED, BUTTON_NAME
from homeassistant.const import CONF_HOST, CONF_PORT
from ..reconnecting_client import ReconnectingClient
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'lirc_network'
REMOTE_NAME = 'remote'
REPEAT_COUNTER = 'repeat_counter'
LIRC_HOST = 'host'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema(vol.All([{
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=8765): cv.port,
}]))
}, extra=vol.ALLOW_EXTRA)
async def async_setup(hass, config):
_LOGGER.warning(f"Config is {config[DOMAIN]}")
for config_element in config[DOMAIN]:
connection = LircConnection(hass, config_element)
await connection.start()
return True
class LircConnection(ReconnectingClient):
def __init__(self, hass, config):
super().__init__(hass, config[CONF_HOST], config[CONF_PORT], "lirc_network",
receive_line_callback=self._process_line,
connection_status_changed_callback=self._connection_state_changed)
async def _connection_state_changed(self, _):
pass
async def _process_line(self, line):
# Example msg:
# 0000000000001795 00 Down Hauppauge_350
# 0000000000001795 01 Down Hauppauge_350
splitted_line = line.split()
if len(splitted_line) != 4:
_LOGGER.warning(f'Ignoring LIRC message from host {self._host}: "{line}"')
return
else:
code, repeat_counter, key_name, remote_name = splitted_line
repeat_counter = int(repeat_counter, 16) # repeat code is hexadecimal
key_name = key_name.lower()
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

@ -0,0 +1,8 @@
{
"domain": "lirc_network",
"name": "Lirc Network",
"documentation": "",
"requirements": [],
"dependencies": [],
"codeowners": ["@mabau"]
}

View File

@ -0,0 +1,73 @@
import asyncio
import logging
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
_LOGGER = logging.getLogger(__name__)
class ReconnectingClient:
def __init__(self, hass, host, port, connection_name, receive_line_callback, connection_status_changed_callback):
self.connected = False
self.reconnect_time_start = 1
self.reconnect_time_max = 60
self.reconnect_time = self.reconnect_time_start
self.hass = hass
self._host = host
self._port = port
self._receive_line_callback = receive_line_callback
self._connection_status_changed_callback = connection_status_changed_callback
self._run = False
self._writer = None
self._connection_last_state = 'UNKNOWN'
self._connection_name = connection_name
async def start(self):
self._run = True
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.stop)
self.hass.loop.create_task(self._connection())
async def stop(self):
self._run = False
self.connected = False
def write_line(self, line):
if self._writer:
line += '\n'
self._writer.write(line.encode())
async def _connection(self):
try:
reader, writer = await asyncio.open_connection(self._host, self._port)
_LOGGER.info("Connected to {} {}:{}".format(self._connection_name, self._host, self._port))
self._connection_last_state = 'CONNECTED'
self._writer = writer
self.connected = True
await self._connection_status_changed_callback('connected')
self.reconnect_time = self.reconnect_time_start
writer.writelines([
"displayattr .*\n".encode(),
"inform on\n".encode(),
])
while self._run:
line = await reader.readline()
if not line:
raise OSError("Disconnect")
line = line.decode()
_LOGGER.debug("{} received line: {}".format(self._connection_name, line))
await self._receive_line_callback(line)
except OSError:
if self._connection_last_state != 'FAILED':
notification_text = "{} connection to {}:{} failed".format(self._connection_name,self._host, self._port)
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))
await self._connection_status_changed_callback('disconnected')
self._connection_last_state = 'FAILED'
self.connected = False
await asyncio.sleep(self.reconnect_time)
self.reconnect_time = min(2 * self.reconnect_time, self.reconnect_time_max)
self.hass.loop.create_task(self._connection())

View File

@ -0,0 +1 @@
"""Support for IBM Watson TTS integration."""

View File

@ -0,0 +1,12 @@
{
"domain": "watson_tts",
"name": "IBM Watson TTS",
"documentation": "https://www.home-assistant.io/components/watson_tts",
"requirements": [
"ibm-watson==3.0.3"
],
"dependencies": [],
"codeowners": [
"@rutkai"
]
}

View File

@ -0,0 +1,148 @@
"""Support for IBM Watson TTS integration."""
import logging
import voluptuous as vol
from homeassistant.components.tts import PLATFORM_SCHEMA, Provider
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
CONF_URL = 'watson_url'
CONF_APIKEY = 'watson_apikey'
ATTR_CREDENTIALS = 'credentials'
DEFAULT_URL = 'https://stream.watsonplatform.net/text-to-speech/api'
CONF_VOICE = 'voice'
CONF_OUTPUT_FORMAT = 'output_format'
CONF_OUTPUT_AUDIO_RATE = 'output_audio_rate'
CONF_TEXT_TYPE = 'text'
# List from https://tinyurl.com/watson-tts-docs
SUPPORTED_VOICES = [
"de-DE_BirgitVoice",
"de-DE_BirgitV2Voice",
"de-DE_DieterVoice",
"de-DE_DieterV2Voice",
"en-GB_KateVoice",
"en-US_AllisonVoice",
"en-US_AllisonV2Voice",
"en-US_LisaVoice",
"en-US_LisaV2Voice",
"en-US_MichaelVoice",
"en-US_MichaelV2Voice",
"es-ES_EnriqueVoice",
"es-ES_LauraVoice",
"es-LA_SofiaVoice",
"es-US_SofiaVoice",
"fr-FR_ReneeVoice",
"it-IT_FrancescaVoice",
"it-IT_FrancescaV2Voice",
"ja-JP_EmiVoice",
"pt-BR_IsabelaVoice"
]
SUPPORTED_OUTPUT_FORMATS = [
'audio/flac',
'audio/mp3',
'audio/mpeg',
'audio/ogg',
'audio/ogg;codecs=opus',
'audio/ogg;codecs=vorbis',
'audio/wav'
]
CONTENT_TYPE_EXTENSIONS = {
'audio/flac': 'flac',
'audio/mp3': 'mp3',
'audio/mpeg': 'mp3',
'audio/ogg': 'ogg',
'audio/ogg;codecs=opus': 'ogg',
'audio/ogg;codecs=vorbis': 'ogg',
'audio/wav': 'wav',
}
DEFAULT_VOICE = 'en-US_AllisonVoice'
DEFAULT_OUTPUT_FORMAT = 'audio/mp3'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_URL, default=DEFAULT_URL): cv.string,
vol.Required(CONF_APIKEY): cv.string,
vol.Optional(CONF_VOICE, default=DEFAULT_VOICE): vol.In(SUPPORTED_VOICES),
vol.Optional(CONF_OUTPUT_FORMAT, default=DEFAULT_OUTPUT_FORMAT):
vol.In(SUPPORTED_OUTPUT_FORMATS),
vol.Optional(CONF_OUTPUT_AUDIO_RATE): cv.positive_int,
})
def get_engine(hass, config):
"""Set up IBM Watson TTS component."""
from ibm_watson import TextToSpeechV1
service = TextToSpeechV1(
url=config[CONF_URL],
iam_apikey=config[CONF_APIKEY]
)
supported_languages = list({s[:5] for s in SUPPORTED_VOICES})
default_voice = config[CONF_VOICE]
output_format = config[CONF_OUTPUT_FORMAT]
output_audio_rate = config.get(CONF_OUTPUT_AUDIO_RATE, None)
return WatsonTTSProvider(
service, supported_languages, default_voice,
output_format, output_audio_rate)
class WatsonTTSProvider(Provider):
"""IBM Watson TTS api provider."""
def __init__(self,
service,
supported_languages,
default_voice,
output_format,
output_audio_rate):
"""Initialize Watson TTS provider."""
self.service = service
self.supported_langs = supported_languages
self.default_lang = default_voice[:5]
self.default_voice = default_voice
self.output_format = output_format
self.output_audio_rate = output_audio_rate
self.name = 'Watson TTS'
@property
def supported_languages(self):
"""Return a list of supported languages."""
return self.supported_langs
@property
def default_language(self):
"""Return the default language."""
return self.default_lang
@property
def default_options(self):
"""Return dict include default options."""
return {CONF_VOICE: self.default_voice}
@property
def supported_options(self):
"""Return a list of supported options."""
return [CONF_VOICE]
def get_tts_audio(self, message, language=None, options=None):
"""Request TTS file from Watson TTS."""
output_format = self.output_format
if self.output_audio_rate:
output_format += f";rate={self.output_audio_rate}"
response = self.service.synthesize(
message, accept=output_format,
voice=self.default_voice).get_result()
return (CONTENT_TYPE_EXTENSIONS[self.output_format],
response.content)

2
todo
View File

@ -1,4 +1,6 @@
- watson TTS add new RATE configuration and create merge request [ok]
- add brighter/darker action (service!) - add brighter/darker action (service!)