From 08c18b3deecccf050b4d5712d4fc14eea40e9713 Mon Sep 17 00:00:00 2001 From: Martin Bauer Date: Sun, 22 Dec 2019 19:21:12 +0100 Subject: [PATCH] Rule creation for IR remotes --- config_creation/groups.yaml | 14 +- config_creation/ir_automations.py | 226 ++++++++++++++++++ config_creation/knx_conf.py | 4 +- config_creation/main.py | 50 ++-- .../squeezebox_telnet/media_player.py | 4 + custom_components/squeezebox_telnet/radio.py | 68 ++++++ scenes.yaml | 57 ++++- 7 files changed, 400 insertions(+), 23 deletions(-) create mode 100644 config_creation/ir_automations.py create mode 100644 custom_components/squeezebox_telnet/radio.py diff --git a/config_creation/groups.yaml b/config_creation/groups.yaml index 563fe72..5b9de3d 100644 --- a/config_creation/groups.yaml +++ b/config_creation/groups.yaml @@ -1,8 +1,8 @@ # Add only entities here that are auto-discovered (not FHEM and KNX devices) -default_view: - view: true - icon: mdi:home +#default_view: +# view: true +# icon: mdi:home living_area: name: Wohnbereich @@ -30,9 +30,9 @@ hallway: - light.gang_bogen - light.gang_einganglicht - light.gang_licht - - switch.bewegungsmelder_west_led - - switch.bewegungsmelder_ost_led - - switch.bewegungsmelder_mitte_led + - light.bewegungsmelder_west_led + - light.bewegungsmelder_ost_led + - light.bewegungsmelder_mitte_led outside: name: Außen @@ -66,5 +66,7 @@ other: first_floor: name: Oben + entities: + - light.wohnzimmer_stehlampe_oben diff --git a/config_creation/ir_automations.py b/config_creation/ir_automations.py new file mode 100644 index 0000000..f4e0f6e --- /dev/null +++ b/config_creation/ir_automations.py @@ -0,0 +1,226 @@ +import re +import os +from ruamel.yaml import YAML + +yaml = YAML() + + +# -------------------------------------- put the config here ----------------------------------------------------------- + +def get_config(): + return { + 'bedroom': { + 'ir_host': 'bedroompi', + 'media_player': 'media_player.bedroompi', + 'group': 'group.bedroom', + + 'mapping': { + 'key_1': '[playlist] Good Morning', + 'key_2': '[playlist] Good Night Long', + 'key_3': '[playlist] Good Night', + + 'key_4': '[playlist] Bar Classics', + 'key_5': '[playlist] Sentimental Moods', + 'key_6': '[playlist] Pop', + + 'key_7': '[radio] B 5 aktuell', + 'key_8': '[radio] BR-Klassik', + 'key_9': '[playlist] http://opml.radiotime.com/Tune.ashx?id=s25028', # Klassik Radio + + 'key_numeric_star': '[radio] Antenne Bayern', + 'key_0': '[radio] Bayern 3', + 'key_numeric_pound': '[radio] Bayern 2', + + 'key_red': '[scene] schlafzimmer_orange', + 'key_green': '[scene] schlafzimmer_rot', + 'key_yellow': '[scene] schlafzimmer_ganz_hell', + 'key_blue': '[scene] schlafzimmer_blau', + + 'key_tv': '[timed_light_off] 30', + 'key_video': '[timed_light_off] 15', + 'key_music': '[timed_light_off] 10', + 'key_pictures': '[timed_light_off] 5', + + 'key_power': [ # Music & Lights off + service('media_player.media_pause', 'media_player.bedroompi'), + service('light.turn_off', 'group.bedroom'), + ], + 'key_ok': [ # Grosser Rollo zu, kleiner halb zu + service('cover.close_cover', 'cover.schlafzimmer_rollo_gross'), + service('cover_half.set_half', 'cover.schlafzimmer_rollo_klein'), + ], + 'key_mute': [service('light.turn_off', 'group.all_downstairs_but_bedroom_and_outside')], + 'key_channel': [service('light.turn_off', 'group.all_downstairs_but_bedroom')], + } + }, + 'living_area': { + 'ir_host': 'kitchenpi', + 'media_player': 'media_player.kitchenpi', + 'group': 'group.living_area', + + 'mapping': { + 'key_4': '[playlist] Bar Classics', + 'key_5': '[playlist] Sentimental Moods', + 'key_6': '[playlist] Pop', + + 'key_7': '[radio] B 5 aktuell', + 'key_8': '[radio] BR-Klassik', + 'key_9': '[playlist] http://opml.radiotime.com/Tune.ashx?id=s25028', # Klassik Radio + + 'key_numeric_star': '[radio] Antenne Bayern', + 'key_0': '[radio] Bayern 3', + 'key_numeric_pound': '[radio] Bayern 2', + + 'key_red': '[scene] wohnbereich_orange', + 'key_green': '[scene] wohnbereich_grun', + 'key_yellow': '[scene] wohnbereich_hell', + 'key_blue': '[scene] wohnbereich_blau_grun', + } + } + } + + +# ---------------------------------------------------------------------------------------------------------------------- + + +description_regex = re.compile(r'\[\s*(.*)\s*\](.*)') + + +def split_description(d): + res = description_regex.match(d) + return res.group(1).strip(), res.group(2).strip() + + +def automation_from_config(ir_description): + ir_host = ir_description['ir_host'] + media_player = ir_description['media_player'] + group = ir_description['group'] + + result = [] + for key, description in ir_description['mapping'].items(): + automation = {'alias': f'IR {ir_host} {key}', + 'trigger': ir_trigger(ir_host, key)} + if isinstance(description, list): + action = description + elif isinstance(description, str): + function, value = split_description(description) + if function == 'playlist': + action = service('media_player.play_media', media_player, media_content_id=value) + elif function == 'radio': + action = service('media_player.play_media', media_player, + media_content_id=value, media_content_type='channel') + elif function == 'scene': + action = service('scene.turn_on', 'scene.' + value) + elif function == 'timed_light_off': + action = service('light.turn_off', group, transition=str(60 * int(value))) + else: + raise ValueError("Invalid prefix " + function) + else: + raise ValueError("Invalid type for entry " + key) + + automation['action'] = action + result.append(automation) + return result + + +def ir_trigger(ir_host, button_name): + return { + 'platform': 'event', + 'event_type': 'ir_command_received', + 'button_name': button_name, + 'repeat_counter': 0, + 'host': ir_host, + } + + +def service(service_name, entity_id, **kwargs): + kwargs['entity_id'] = entity_id + return { + 'service': service_name, + 'data': kwargs + } + + +def default_shutter_controls(device_group, ir_host): + """Default rules for Hauppauge IR for Shutter control with up, down, left, right buttons""" + return [ + { + 'alias': f'IR {ir_host} Rollo auf', + 'trigger': ir_trigger(ir_host, 'key_up'), + 'action': service('cover.open_cover', device_group), + }, + { + 'alias': f'IR {ir_host} Rollo zu', + 'trigger': ir_trigger(ir_host, 'key_down'), + 'action': service('cover.close_cover', device_group), + }, + { + 'alias': f'IR {ir_host} Rollo halb', + 'trigger': [ir_trigger(ir_host, 'key_left'), ir_trigger(ir_host, 'key_right')], + 'action': service('cover_half.set_half', device_group), + }, + ] + + +def default_light_controls(device_group, ir_host): + """Default light rules for Hauppauge IR for light dimming with channel up/down and light off with stop button""" + return [ + { + 'alias': f'IR {ir_host} Licht heller', + 'trigger': ir_trigger(ir_host, 'key_channelup'), + 'action': service('dimmer.dim', device_group, offset=30), + }, + { + 'alias': f'IR {ir_host} Licht dunkler', + 'trigger': ir_trigger(ir_host, 'key_channeldown'), + 'action': service('dimmer.dim', device_group, offset=-30), + }, + { + 'alias': f'IR {ir_host} Licht viel heller', + 'trigger': ir_trigger(ir_host, 'key_menu'), + 'action': service('dimmer.dim', device_group, offset=130), + }, + { + 'alias': f'IR {ir_host} Licht viel dunkler', + 'trigger': ir_trigger(ir_host, 'key_stop'), + 'action': service('dimmer.dim', device_group, offset=-130), + }, + { + 'alias': f'IR {ir_host} Licht aus', + 'trigger': ir_trigger(ir_host, 'key_goto'), + 'action': service('light.turn_off', device_group), + }, + ] + + +def default_music_controls(device_group, ir_host): + """Default music control (play, pause, next) for Hauppauge IR""" + return [ + { + 'alias': f'IR {ir_host} Musik Play/Pause', + 'trigger': [ir_trigger(ir_host, 'key_play'), ir_trigger(ir_host, 'key_pause')], + 'action': service('media_player.media_play_pause', device_group), + }, + { + 'alias': f'IR {ir_host} Musik Next', + 'trigger': [ir_trigger(ir_host, 'key_forward'), ir_trigger(ir_host, 'key_fastforward')], + 'action': service('media_player.media_next_track', device_group), + }, + { + 'alias': f'IR {ir_host} Musik Prev', + 'trigger': [ir_trigger(ir_host, 'key_previous'), ir_trigger(ir_host, 'key_rewind')], + 'action': service('media_player.media_next_track', device_group), + }, + ] + + +def create_rules(folder): + for name, data in get_config().items(): + rules = automation_from_config(data) + rules += default_light_controls(data['group'], data['ir_host']) + rules += default_music_controls(data['media_player'], data['ir_host']) + rules += default_shutter_controls(data['group'], data['ir_host']) + file_name = os.path.join(folder, name + '.yaml') + with open(file_name, 'w') as f: + f.write("# Dont' edit manually! this is generated!\n\n") + yaml.dump(rules, f) diff --git a/config_creation/knx_conf.py b/config_creation/knx_conf.py index 415c9c9..de819c3 100644 --- a/config_creation/knx_conf.py +++ b/config_creation/knx_conf.py @@ -123,7 +123,9 @@ def create_lights(device_info: List[DeviceInfo], result = [] for entry in device_info: try: - on_off_write_addr = csv_contents[entry.csv_name + postfix_on_off_write] + on_off_write_addr = csv_contents.get(entry.csv_name + postfix_on_off_write, None) + if on_off_write_addr is None: + on_off_write_addr = csv_contents[entry.csv_name] on_off_read_addr = csv_contents.get(entry.csv_name + postfix_on_off_read, None) brightness_write_addr = csv_contents.get(entry.csv_name + postfix_brightness_write, None) brightness_read_addr = csv_contents.get(entry.csv_name + postfix_brightness_read, None) diff --git a/config_creation/main.py b/config_creation/main.py index 7a778e4..61ce082 100644 --- a/config_creation/main.py +++ b/config_creation/main.py @@ -3,6 +3,7 @@ import argparse from util import DeviceInfo, add_to_group, name_to_id from ruamel.yaml import YAML import knx_conf as knx +from ir_automations import create_rules as create_automation_rules script_path = os.path.dirname(os.path.realpath(__file__)) yaml = YAML() @@ -48,10 +49,14 @@ def add_knx_devices(devices, groups): DeviceInfo("LichtWaschküche", "Waschküche Licht", 'hallway'), # Normal lights DeviceInfo('AussenleuchteHaustüren', 'Haustür Licht', 'outside'), - DeviceInfo('AussenleuchteObenNW', 'Haustür Licht NW', 'outside'), + DeviceInfo('AussenleuchteObenNW', 'Haustür Licht NW', 'first_floor'), DeviceInfo('TreppenhausLicht', "Treppenhaus Licht", 'first_floor'), DeviceInfo('WCLicht', "WC Licht", 'other'), DeviceInfo('LampeVorratsraum', "Vorratsraum Licht", 'other'), + # Bewegungsmelder LEDs + DeviceInfo("BewegungsmelderMitte LED", "Bewegungsmelder Mitte LED", 'hallway'), + DeviceInfo("BewegungsmelderWest LED", "Bewegungsmelder West LED", 'hallway'), + DeviceInfo("BewegungsmelderOst LED", "Bewegungsmelder Ost LED", 'hallway'), ] shutters = [ @@ -66,10 +71,6 @@ def add_knx_devices(devices, groups): DeviceInfo("KlingelOben", "Klingel Oben", 'first_floor'), DeviceInfo("Klingel Innen", "Klingel Innentür", 'other'), DeviceInfo("Klingel Aussen", "Klingen Außentür", 'other'), - # Bewegungsmelder LEDs - DeviceInfo("BewegungsmelderMitte LED", "Bewegungsmelder Mitte LED", 'hallway'), - DeviceInfo("BewegungsmelderWest LED", "Bewegungsmelder West LED", 'hallway'), - DeviceInfo("BewegungsmelderOst LED", "Bewegungsmelder Ost LED", 'hallway'), ] scene_button_names = ['ObenLinks', 'ObenRechts', 'MitteLinks', 'MitteRechts', 'UntenLinks', 'UntenRechts'] scene_button_names = [(i, e) for i, e in enumerate(scene_button_names)] @@ -174,15 +175,33 @@ def add_fhem_devices(devices, groups): devices[device_type].append(device) -def add_light_groups(groups): - light_groups = { - f"light_{group_id}": { - 'name': content['name'] + " Lichter", - 'entities': [e for e in content['entities'] if e.startswith('light.')], - } - for group_id, content in groups.items() if 'name' in content +def add_meta_groups(groups): + all_devices = set() + for group in groups.values(): + all_devices.update(group['entities']) + first_floor = set(groups['first_floor']['entities']) + bedroom = set(groups['bedroom']['entities']) + outside = set(groups['outside']['entities']) + + groups['all_downstairs'] = { + 'name': 'Unten Alles', + 'entities': [e for e in all_devices - first_floor] + } + + groups['all_downstairs_but_outside'] = { + 'name': 'Unten Alles Ausser Draussen', + 'entities': [e for e in all_devices - first_floor - outside] + } + + groups['all_downstairs_but_bedroom'] = { + 'name': 'Unten Alles Ausser Schlafzimmer', + 'entities': [e for e in all_devices - first_floor - bedroom], + } + + groups['all_downstairs_but_bedroom_and_outside'] = { + 'name': 'Unten Alles Ausser Schlafzimmer und Draussen', + 'entities': [e for e in all_devices - first_floor - bedroom - outside], } - groups.update(light_groups) def make_sensor_exclude_list(all_devices, name_fragments): @@ -256,8 +275,9 @@ def create_config(target_directory, development=False): yaml.dump(all_devices, output) yaml.dump(logbook_config(all_devices), output) yaml.dump(recorder_config(all_devices), output) + output.write("automation manual: !include_dir_merge_list automations\n") - add_light_groups(group_dict) + add_meta_groups(group_dict) with open(os.path.join(target_directory, 'groups.yaml'), 'w') as output: output.write("# Dont' edit manually! this is generated!\n\n") @@ -270,6 +290,8 @@ def create_config(target_directory, development=False): additional_file = 'secrets_development.yaml' if development else 'secrets_deploy.yaml' output.write(open(os.path.join(script_path, additional_file), 'r').read()) + create_automation_rules(os.path.join(target_directory, 'automations')) + if __name__ == '__main__': parser = argparse.ArgumentParser() diff --git a/custom_components/squeezebox_telnet/media_player.py b/custom_components/squeezebox_telnet/media_player.py index f158f64..b02c311 100644 --- a/custom_components/squeezebox_telnet/media_player.py +++ b/custom_components/squeezebox_telnet/media_player.py @@ -6,6 +6,7 @@ import sys from typing import List, Tuple import voluptuous as vol from ..reconnecting_client import ReconnectingClient +from .radio import find_local_radio_url_by_name from homeassistant.components.media_player import ( MediaPlayerDevice, PLATFORM_SCHEMA) @@ -485,6 +486,9 @@ class SqueezeBoxDevice(MediaPlayerDevice): If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the current playlist. This method must be run in the event loop and returns a coroutine. """ + if media_type == 'channel': + media_id = find_local_radio_url_by_name(media_id) + if kwargs.get(ATTR_MEDIA_ENQUEUE): return self._add_uri_to_playlist(media_id) diff --git a/custom_components/squeezebox_telnet/radio.py b/custom_components/squeezebox_telnet/radio.py new file mode 100644 index 0000000..bf4cd26 --- /dev/null +++ b/custom_components/squeezebox_telnet/radio.py @@ -0,0 +1,68 @@ +import xml.etree.ElementTree as ET +from urllib.request import urlopen + +ompl_radio_browse_url = 'http://opml.radiotime.com/Browse.ashx' + + +def radio_name_cleanup(radio_name): + """Removes tokens with brackets or points + "Radio Bamberg 106.1 (Top 40/Pop)" -> "Radio Bamberg 106.1" + """ + radio_name = radio_name.split() + res = [] + for token in radio_name: + if '(' in token or ')' in token: + continue + res.append(token) + return " ".join(res) + + +def get_sender_information(queryURL): + """ + Example Query: GET http://opml.radiotime.com/Browse.ashx?partnerId=&serial= + partnerid and serial does not seem to be necessary - so we do not use it here, + """ + xmlfile = urlopen(queryURL) + tree = ET.ElementTree(file=xmlfile) + rootnode = tree.getroot() + + result = dict() + for node in rootnode.iter('outline'): + if 'type' not in node.attrib or node.attrib['type'] != "audio": + continue + station_name = radio_name_cleanup(node.attrib['text']) + station_url = node.attrib['URL'] + station_id = node.attrib['guide_id'] + print(station_name) + result[station_id] = {'name': station_name, 'url': station_url} + + return result + + +def get_related_radio_stations(stationID): + query_url = ompl_radio_browse_url + '?id=' + stationID + return get_sender_information(query_url) + + +def get_local_radio_stations(): + # goto ompl.radiotime with parameter ?c=local and look for your city there then paste id here + query_url = ompl_radio_browse_url + '?id=r100765' + result = get_sender_information(query_url) + print(result) + return result + + +def find_local_radio_url_by_name(search_name): + """Searches at the Radiotime database for a local radiostation which contains the searchName """ + search_name = search_name.lower().strip() + if search_name.startswith('http'): + return search_name + + for radioId, radio in find_local_radio_url_by_name.stations.items(): + if search_name in radio['name'].lower(): + return radio['url'] + + raise ValueError("Could not find radio station " + search_name) + + +find_local_radio_url_by_name.stations = get_local_radio_stations() diff --git a/scenes.yaml b/scenes.yaml index bbbb93b..3e4b549 100644 --- a/scenes.yaml +++ b/scenes.yaml @@ -202,6 +202,51 @@ xy_color: [0.2075, 0.6584] +- name: Wohnbereich Hell + entities: + light.esszimmer_deckenlampe_west: + state: on + brightness: 255 + light.wohnzimmer_deckenlampe: + state: on + brightness: 255 + light.kuche_deckenlampe: + state: on + brightness: 255 + light.wohnzimmer_regal_links: + state: on + brightness: 255 + xy_color: [0.527, 0.447] + light.kuche_links: + state: on + brightness: 255 + xy_color: [0.537, 0.438] + light.kuche_rechts: + state: on + brightness: 255 + xy_color: [0.557, 0.403] + light.wohnzimmer_regal_rechts: + state: on + brightness: 255 + xy_color: [0.616, 0.371] + light.wohnzimmer_stehlampe_oben: + state: on + brightness: 255 + color_temp: 492 + light.wohnzimmer_stehlampe: + state: on + brightness: 255 + color_temp: 492 + light.wohnzimmer_kugel: + state: on + brightness: 255 + color_temp: 492 + light.kuche_vorne: + state: on + brightness: 255 + color_temp: 492 + + - name: Wohnbereich Meditation entities: light.kuche_deckenlampe: @@ -330,12 +375,20 @@ - name: Schlafzimmer Bettlicht dunkel entities: + light.schlafzimmer_deckenlampe: + state: off + light.schlafzimmer_schrank: + state: off light.bett_martin: state: on - brightness: 2 + brightness: 5 + xy_color: [0.502, 0.414] light.bett_rebecca: state: on - brightness: 2 + brightness: 5 + xy_color: [0.502, 0.414] + light.schlafzimmer_fluter: + state: off - name: Schlafzimmer Orange