diff --git a/config_creation/manual_config.yaml b/config_creation/manual_config.yaml index 4d1d75f..afcf74f 100644 --- a/config_creation/manual_config.yaml +++ b/config_creation/manual_config.yaml @@ -31,17 +31,13 @@ dimmer: # Weather prediction # - platform: yr -# Text to speech -#tts: -## - platform: google_translate -# language: 'de' -# base_url: http://192.168.178.73:8123 tts: - platform: watson_tts watson_apikey: X_tnnoaZGOwxZlqUn07wkD2G-0vaaAuOw6I6d_6jpCf7 watson_url: https://gateway-lon.watsonplatform.net/text-to-speech/api voice: de-DE_BirgitVoice - output_format: audio/flac;rate=44100 + output_format: audio/flac + output_audio_rate: 44100 knx: rate_limit: 20 @@ -57,6 +53,13 @@ fhem: cul_device_name: CUL_HM +lirc_network: + - host: kitchenpi + port: 2222 + - host: bedroompi + port: 2222 + + media_player: - platform: squeezebox host: server @@ -66,7 +69,6 @@ media_player: - group: !include groups.yaml automation: !include automations.yaml script: !include scripts.yaml diff --git a/custom_components/fhem/__init__.py b/custom_components/fhem/__init__.py index e7b16d6..c94c823 100644 --- a/custom_components/fhem/__init__.py +++ b/custom_components/fhem/__init__.py @@ -2,10 +2,10 @@ import logging import voluptuous as vol -import asyncio from collections import defaultdict 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__) CONF_CUL_DEVICE_NAME = 'cul_device_name' @@ -23,28 +23,20 @@ CONFIG_SCHEMA = vol.Schema({ async def async_setup(hass, config): - connection = FhemConnection(hass, config) + connection = FhemConnection(hass, config[DOMAIN]) hass.data[DATA_FHEM] = connection await connection.start() return True -class FhemConnection: +class FhemConnection(ReconnectingClient): def __init__(self, hass, config): - self.hass = hass - self._host = config[DOMAIN][CONF_HOST] - self._port = config[DOMAIN][CONF_PORT] - self._cul_device_name = config[DOMAIN][CONF_CUL_DEVICE_NAME] - - self.connected = False - self.reconnect_time_start = 1 - self.reconnect_time_max = 60 - self.reconnect_time = self.reconnect_time_start + super().__init__(hass, config[CONF_HOST], config[CONF_PORT], "FHEM", + receive_line_callback=self._process_line, + connection_status_changed_callback=self._update_all_devices) + self._cul_device_name = config[CONF_CUL_DEVICE_NAME] self._devices = defaultdict(list) - self._run = False - self._writer = None - self._connection_last_state = 'UNKNOWN' def register_device(self, id, d): self._devices[id].append(d) @@ -53,56 +45,11 @@ class FhemConnection: "displayattr .*\n".encode(), ]) - async def _update_all_devices(self): + async def _update_all_devices(self, _): for device_list in self._devices.values(): for device in device_list: 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): if line.startswith(self._cul_device_name + " "): # Status update message _, device_name, command = line.split(" ", 2) @@ -127,7 +74,7 @@ class FhemConnection: :param arguments: string or list of strings containing command parameters """ 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): diff --git a/custom_components/lirc_network/__init__.py b/custom_components/lirc_network/__init__.py new file mode 100644 index 0000000..5d3fe21 --- /dev/null +++ b/custom_components/lirc_network/__init__.py @@ -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) diff --git a/custom_components/lirc_network/manifest.json b/custom_components/lirc_network/manifest.json new file mode 100644 index 0000000..a2961a4 --- /dev/null +++ b/custom_components/lirc_network/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "lirc_network", + "name": "Lirc Network", + "documentation": "", + "requirements": [], + "dependencies": [], + "codeowners": ["@mabau"] +} diff --git a/custom_components/reconnecting_client.py b/custom_components/reconnecting_client.py new file mode 100644 index 0000000..dae32b1 --- /dev/null +++ b/custom_components/reconnecting_client.py @@ -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()) diff --git a/custom_components/watson_tts/__init__.py b/custom_components/watson_tts/__init__.py new file mode 100644 index 0000000..abdc930 --- /dev/null +++ b/custom_components/watson_tts/__init__.py @@ -0,0 +1 @@ +"""Support for IBM Watson TTS integration.""" diff --git a/custom_components/watson_tts/manifest.json b/custom_components/watson_tts/manifest.json new file mode 100644 index 0000000..d40baac --- /dev/null +++ b/custom_components/watson_tts/manifest.json @@ -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" + ] +} diff --git a/custom_components/watson_tts/tts.py b/custom_components/watson_tts/tts.py new file mode 100644 index 0000000..f34c094 --- /dev/null +++ b/custom_components/watson_tts/tts.py @@ -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) diff --git a/todo b/todo index ac64dc1..2a0ddd6 100644 --- a/todo +++ b/todo @@ -1,4 +1,6 @@ +- watson TTS add new RATE configuration and create merge request [ok] + - add brighter/darker action (service!)