├── .gitignore ├── README.md ├── screenshots ├── humidity.png ├── teatimer.png ├── weather.png ├── weather2.png └── windows.png ├── uhumidity.py ├── ulanzi.py ├── uproximity.py ├── ustopwatch.py ├── uweather.py └── uwindow.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | private 3 | *.yaml -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ulanzi TC001 Appdaemon Flows 2 | 3 | This is a collection of [Appdaemon](https://appdaemon.readthedocs.io/en/latest/index.html) Apps to drive a [Ulanzi Desktop Clock (TC001)](https://www.ulanzi.com/products/ulanzi-pixel-smart-clock-2882) with the [Awtrix Light](https://github.com/Blueforcer/awtrix-light) firmware. 4 | 5 | For any information regarding installing and using Appdaemon with Homeassistant, please consult the documentation. 6 | 7 | ## Basic configuration of all apps 8 | 9 | All apps here make use of a base class to abstract away the Ulanzi protocol and common settings. Therefore some settings have to be defined in the same way in all apps: 10 | 11 | ```yaml 12 | MyApp: 13 | mqtt_prefix: awtrix_XXXX # The MQTT prefix where Awtrix Light is listening 14 | icon: "1337" # Icon to use for the app. Needs to be installed on the device 15 | sound: "tri" # Sound to play on notifications (optional), needs to be installed on the device 16 | enabled_entity: "input_boolean.myswitch" # Homeassistant switch to enable the app (optional) 17 | ``` 18 | 19 | ## Timer 20 | 21 | The app will automatically pick up on any defined `timer` entities in Homeassistant and display their progress on the clock when they are running or paused. You can define custom icons for particular timers and ignore others and define a sound file to play on completion. 22 | 23 | See the source code if you want to change the colors of the progress bar. 24 | 25 | ![Teatimer](screenshots/teatimer.png) 26 | 27 | Example configuration: 28 | 29 | ```yaml 30 | TimerDisplay: 31 | module: ustopwatch 32 | class: UlanziTimerDisplay 33 | mqtt_prefix: awtrix_XXXX 34 | icon: "42893" 35 | sound: "tri" 36 | custom_icons: 37 | tea: "35123" # Use the entity name without the "timer." prefix 38 | ignore: 39 | - front_porch_light # ... also here 40 | ``` 41 | 42 | ## Proximity 43 | 44 | Will display the proximity of different persons to the home when they are not at a defined location. Makes use of the Homeassistant `device_tracker` and `proximity` integrations to display proximity and direction of travel. 45 | 46 | Example configuration: 47 | 48 | ```yaml 49 | MyProximity: 50 | module: uproximity 51 | class: UlanziProximityInfo 52 | mqtt_prefix: awtrix_XXXX 53 | icon: "5869" 54 | tracker: device_tracker.myphone 55 | proximity_sensor: proximity.me 56 | person: ict # Friendly name to display: "ict: 1.2 km <<<" 57 | ``` 58 | 59 | ## Weather 60 | 61 | A basic weather display. Shows current conditions (with optional second sensor for local measurements) and today's (it it is before 18:00) or tomorrow's forecast with a progress bar for the probability of rain. Has only been tested with the OpenWeathermap integration. 62 | 63 | > [!NOTE] 64 | > You will need to adapt the icon definitions in the source code for the different weather states to your needs and download all defined icons for the best effect. Some icons from the LaMetric library are broken on Awtrix Light and need to be re-saved without compression to look nice. 65 | 66 | ![Current Weather](screenshots/weather.png) 67 | ![Forecast](screenshots/weather2.png) 68 | 69 | Example configuration: 70 | 71 | ```yaml 72 | UlanziWeather: 73 | module: uweather 74 | class: UlanziWeather 75 | mqtt_prefix: awtrix_XXXX 76 | icon: "0" # Not used in this app since we use different icons for all conditions 77 | weather_entity: weather.openweathermap 78 | current_temp_sensor: sensor.balkon_temperature # optional 79 | ``` 80 | 81 | ## Window Status 82 | 83 | Displays open windows. 84 | 85 | ![Windows](screenshots/windows.png) 86 | 87 | Example configuration: 88 | 89 | ```yaml 90 | UlanziWindowStatus: 91 | module: uwindow 92 | class: UlanziWindowAlert 93 | icon: "47934" 94 | mqtt_prefix: awtrix_XXXX 95 | windows: 96 | - entity: binary_sensor.patio_window_contact # Sensor entity 97 | name: Patio # Short name to display 98 | - entity: binary_sensor.pool_room_right_contact 99 | name: Pool R 100 | ``` 101 | 102 | ## Humidity Warning 103 | 104 | Activates an indicator light when humidity in a defined room reaches a threshold. It will display these rooms once when they reach the threshold and can also display the list again on a keypess. 105 | 106 | ![Humidity](screenshots/humidity.png) 107 | 108 | Example configuration: 109 | 110 | ```yaml 111 | HumidityNotifier: 112 | module: uhumidity 113 | class: UlanziHumidityWarning 114 | mqtt_prefix: awtrix_XXXX 115 | icon: "2423" 116 | sensors: 117 | - entity: sensor.pool_sensor_humidity 118 | name: Pool 119 | - entity: sensor.library_sensor_humidity 120 | name: Library 121 | threshold: 60 122 | indicator: light.awtrix_XXXX_indicator_2 # indicator to use 123 | indicator_color: [130, 192, 255] # color of indicator (RGB), optional 124 | show_buttons: # list of entities that, when switched to 'on', will re-display the room-list 125 | - binary_sensor.awtrix_XXX_button_select # middle button of the clock 126 | - input_button.ulanzi_show_hum 127 | ``` -------------------------------------------------------------------------------- /screenshots/humidity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ict/ulanzi-awtrix-appdaemon/478a214c0c40c1f86b325c500917a4306c3d92b4/screenshots/humidity.png -------------------------------------------------------------------------------- /screenshots/teatimer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ict/ulanzi-awtrix-appdaemon/478a214c0c40c1f86b325c500917a4306c3d92b4/screenshots/teatimer.png -------------------------------------------------------------------------------- /screenshots/weather.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ict/ulanzi-awtrix-appdaemon/478a214c0c40c1f86b325c500917a4306c3d92b4/screenshots/weather.png -------------------------------------------------------------------------------- /screenshots/weather2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ict/ulanzi-awtrix-appdaemon/478a214c0c40c1f86b325c500917a4306c3d92b4/screenshots/weather2.png -------------------------------------------------------------------------------- /screenshots/windows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ict/ulanzi-awtrix-appdaemon/478a214c0c40c1f86b325c500917a4306c3d92b4/screenshots/windows.png -------------------------------------------------------------------------------- /uhumidity.py: -------------------------------------------------------------------------------- 1 | from ulanzi import UlanziApp 2 | 3 | 4 | class UlanziHumidityWarning(UlanziApp): 5 | 6 | def initialize(self): 7 | super().initialize() 8 | self.sensors_above = set() 9 | try: 10 | self.sensors = self.args['sensors'] 11 | self.threshold = self.args['threshold'] 12 | self.indicator = self.args['indicator'] 13 | self.indicator_color = self.args.get('indicator_color', [130, 192, 255]) 14 | self.show_buttons = self.args.get('show_buttons', []) 15 | except KeyError as err: 16 | self.error("Failed getting configuration {}".format(err.args[0])) 17 | return 18 | 19 | for sensor in self.sensors: 20 | sensor_entity = sensor['entity'] 21 | sensor_name = sensor['name'] 22 | current_state = self.get_state(sensor_entity) 23 | self.log(f"Current state of {sensor_name}: {current_state}") 24 | try: 25 | if float(current_state) > self.threshold: 26 | self.sensors_above.add(sensor_name) 27 | self.log(f"Adding {sensor_name} to sensors_above") 28 | except ValueError: 29 | self.log(f"{sensor}: Could not parse {current_state} as float") 30 | 31 | self.listen_state(self.sensor_change, sensor_entity, sensor_name=sensor_name) 32 | 33 | if not isinstance(self.show_buttons, list): 34 | self.show_buttons = [self.show_buttons] 35 | for button in self.show_buttons: 36 | self.listen_state(self.show_humidity, button) 37 | self.update_state(False) 38 | 39 | def sensor_change(self, entity, attribute, old, new, kwargs): 40 | sensor_name = kwargs['sensor_name'] 41 | new_sensor = False 42 | 43 | try: 44 | humidity = float(new) 45 | except ValueError: 46 | self.log(f"{entity}: Could not parse {new} as float") 47 | return 48 | if humidity > self.threshold: 49 | # self.log(f"Over thresh") 50 | if sensor_name not in self.sensors_above: 51 | new_sensor = True 52 | self.sensors_above.add(sensor_name) 53 | elif sensor_name in self.sensors_above: 54 | # self.log(f"No longer over thresh") 55 | self.sensors_above.remove(sensor_name) 56 | 57 | self.update_state(new_sensor) 58 | 59 | def update_state(self, new_sensor): 60 | if not self.sensors_above: 61 | self.turn_off(self.indicator) 62 | else: 63 | self.turn_on(self.indicator, rgb_color=self.indicator_color) 64 | if new_sensor: 65 | self.show_humidity('fake', 'params', 'old', 'off', 'kwargs') 66 | 67 | def show_humidity(self, entity, attribute, old, new, kwargs): 68 | self.log(f"old: {old}, new: {new}") 69 | if old == new or new == 'on' or new == 'unavailable': 70 | return # Generally trigger on button release, but not if new value is not meaningful 71 | entries = [] 72 | if self.sensors_above: 73 | # Send a new notification 74 | for sensor in self.sensors: 75 | if sensor['name'] in self.sensors_above: 76 | entries.append(f"{sensor['name']}: {self.get_state(sensor['entity'])}%") 77 | self.send_notification(" | ".join(entries)) 78 | else: 79 | # Show all sensors 80 | for sensor in self.sensors: 81 | entries.append(f"{sensor['name']}: {self.get_state(sensor['entity'])}%") 82 | self.send_notification(" | ".join(entries)) 83 | 84 | def get_app_text(self): 85 | raise NotImplementedError # We are not using base class implementation 86 | -------------------------------------------------------------------------------- /ulanzi.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | import json 3 | import appdaemon.plugins.hass.hassapi as hass 4 | 5 | class UlanziApp(hass.Hass): 6 | """ 7 | Base class for all Ulanzi apps, supports apps as well as notifications 8 | and stores basic configuration 9 | """ 10 | 11 | def initialize(self): 12 | try: 13 | self.prefix = self.args['mqtt_prefix'] 14 | self.icon = self.args['icon'] 15 | self.sound = self.args.get('sound', None) 16 | self.enabled_entity = self.args.get('enabled_entity', None) 17 | except KeyError as err: 18 | self.error("Failed getting configuration {}".format(err.args[0])) 19 | return 20 | 21 | if self.enabled_entity: 22 | self.listen_state(self.delete_app, self.enabled_entity, new='off') 23 | self.listen_state(self.update_app, self.enabled_entity, new='on') 24 | 25 | @property 26 | def app_name(self): 27 | return self.name 28 | 29 | @property 30 | def mqtt_app_topic(self): 31 | return f'{self.prefix}/custom/{self.app_name}' 32 | 33 | @property 34 | def enabled(self): 35 | if self.enabled_entity: 36 | return self.get_state(self.enabled_entity) == 'on' 37 | return True 38 | 39 | def get_additional_properties(self): 40 | """ Override this method to add additional properties to the MQTT payload """ 41 | return {} 42 | 43 | @abstractmethod 44 | def get_app_text(self): 45 | """ Override this method to return the text to be displayed on the Ulanzi """ 46 | pass 47 | 48 | def update_app(self, *args, **kwargs): 49 | """ Call this method from your subclass to update the app on the Ulanzi """ 50 | if not self.enabled: 51 | return 52 | text = self.get_app_text() 53 | additional_properties = self.get_additional_properties() 54 | mqtt_payload = { 55 | 'icon': self.icon, 56 | 'text': text, 57 | 'lifetime': 60*60 # 1 hour failsafe 58 | } 59 | if additional_properties: 60 | mqtt_payload.update(additional_properties) 61 | 62 | # Call homeassistant service to update the app over MQTT 63 | self.log(f"Sending app update for {self.app_name}: {text}") 64 | self.call_service('mqtt/publish', topic=self.mqtt_app_topic, payload=json.dumps(mqtt_payload)) 65 | 66 | def delete_app(self, *args, **kwargs): 67 | self.call_service('mqtt/publish', topic=self.mqtt_app_topic, payload='{}') 68 | 69 | def send_notification(self, message, icon=None, sound=None, **kwargs): 70 | self.log(f"Sending notification: {message}") 71 | payload = { 72 | 'text': message, 73 | 'icon': icon or self.icon, 74 | 'repeat': 2, 75 | } 76 | if self.sound: 77 | payload['sound'] = self.sound 78 | if sound: 79 | payload['sound'] = sound 80 | if kwargs: 81 | payload.update(kwargs) 82 | self.call_service('mqtt/publish', topic=f'{self.prefix}/notify', payload=json.dumps(payload)) 83 | 84 | 85 | -------------------------------------------------------------------------------- /uproximity.py: -------------------------------------------------------------------------------- 1 | from ulanzi import UlanziApp 2 | 3 | 4 | class UlanziProximityInfo(UlanziApp): 5 | 6 | def initialize(self): 7 | super().initialize() 8 | self.last_distance = 0 9 | self.last_location = None 10 | try: 11 | self.tracker = self.args['tracker'] 12 | self.proximity_sensor = self.args['proximity_sensor'] 13 | self.direction_sensor = self.args['direction_sensor'] 14 | self.person = self.args['person'] 15 | self.change_threshold = self.args.get('change_threshold', 10) 16 | except KeyError as err: 17 | self.error("Failed getting configuration {}".format(err.args[0])) 18 | return 19 | 20 | self.listen_state(self.proximity_change, self.proximity_sensor) 21 | self.listen_state(self.proximity_change, self.tracker) 22 | 23 | def proximity_change(self, entity, attribute, old, new, kwargs): 24 | tracker_state = self.get_state(self.tracker) 25 | self.log(f"Tracker state: {tracker_state}") 26 | if tracker_state != 'not_home': 27 | self.delete_app() 28 | self.last_distance = 0 29 | if tracker_state != self.last_location: 30 | self.send_notification(f"{self.person} @ {tracker_state}") 31 | self.last_location = tracker_state 32 | return 33 | 34 | self.last_location = tracker_state 35 | if entity == self.proximity_sensor: 36 | self.log(f"Proximity change: {old} -> {new}") 37 | self.log(f"Last distance: {self.last_distance} | Last location: {self.last_location}") 38 | if old == new or new == self.last_distance: 39 | return 40 | if abs(int(new) - int(self.last_distance)) >= self.change_threshold: 41 | self.last_distance = new 42 | self.update_app() 43 | 44 | 45 | def get_app_text(self): 46 | state = self.get_state(self.proximity_sensor) 47 | dir = self.get_state(self.direction_sensor) 48 | distance = int(state) 49 | if distance < 1000: 50 | unit = 'm' 51 | else: 52 | distance = round(distance / 1000, 1) 53 | unit = 'km' 54 | return f"{self.person}: {distance} {unit} {'<<<' if dir == 'towards' else '>>>'}" 55 | -------------------------------------------------------------------------------- /ustopwatch.py: -------------------------------------------------------------------------------- 1 | import json 2 | from dataclasses import dataclass 3 | from datetime import datetime, timezone 4 | import time 5 | from ulanzi import UlanziApp 6 | 7 | BAR_COLOR_PAUSED = '#deb764' 8 | BAR_COLOR_RUNNING = '#aadb72' 9 | BAR_COLOR_BG = '#373a40' 10 | 11 | 12 | class UlanziTimerDisplay(UlanziApp): 13 | """ 14 | App that listens to HASS timer events and dynamically displays them 15 | """ 16 | 17 | TIMER_STARTED = 'timer.started' 18 | TIMER_FINISHED = 'timer.finished' 19 | TIMER_CANCELLED = 'timer.cancelled' 20 | TIMER_PAUSED = 'timer.paused' 21 | TIMER_RESTARTED = 'timer.restarted' 22 | 23 | @dataclass 24 | class Timer: 25 | status: str 26 | remaining: int 27 | total: int 28 | icon: str 29 | 30 | def get_output(self): 31 | if self.remaining >= 3600: 32 | text = time.strftime('%-H:%M:%S', time.gmtime(self.remaining)) 33 | else: 34 | text = time.strftime('%-M:%S', time.gmtime(max(0, self.remaining))) 35 | result = { 36 | 'icon': self.icon, 37 | 'text': text, 38 | 'progressBC': BAR_COLOR_BG, 39 | 'progressC': BAR_COLOR_RUNNING if self.status == 'running' else BAR_COLOR_PAUSED, 40 | 'progress': int(100 - (self.remaining / self.total * 100)), 41 | 'lifetime': self.remaining + 30 42 | } 43 | if self.remaining < 30: 44 | result['duration'] = 30 45 | return result 46 | 47 | def initialize(self): 48 | super().initialize() 49 | self.timers = {} 50 | self.tick_timer_handle = None 51 | try: 52 | self.custom_icons = self.args.get('custom_icons', {}) 53 | self.ignore_timers = self.args.get('ignore', []) 54 | except KeyError as err: 55 | self.error("Failed getting configuration {}".format(err.args[0])) 56 | return 57 | 58 | timer_events = [ 59 | UlanziTimerDisplay.TIMER_STARTED, 60 | UlanziTimerDisplay.TIMER_FINISHED, 61 | UlanziTimerDisplay.TIMER_CANCELLED, 62 | UlanziTimerDisplay.TIMER_PAUSED, 63 | UlanziTimerDisplay.TIMER_RESTARTED, 64 | ] 65 | self.listen_event(self.trigger, timer_events) 66 | 67 | def _get_seconds_until(self, timestamp): 68 | """Get the amount of seconds until the given timestamp""" 69 | now = datetime.now(timezone.utc) 70 | then = datetime.fromisoformat(timestamp) 71 | return int((then - now).total_seconds()) 72 | 73 | def trigger(self, event_name, data, kwargs): 74 | self.log(f"Received event {event_name}: {data}") 75 | timer_id = data['entity_id'] 76 | timer_name = timer_id.split('.')[-1] 77 | 78 | if timer_id in self.ignore_timers or timer_name in self.ignore_timers: 79 | return 80 | 81 | timer = self.get_state(timer_id, attribute='all') 82 | icon = self.custom_icons.get(timer_name, self.icon) 83 | 84 | if event_name in (UlanziTimerDisplay.TIMER_STARTED): 85 | self.timers[timer_id] = UlanziTimerDisplay.Timer( 86 | status='running', 87 | remaining=self._get_seconds_until(timer['attributes']['finishes_at']), 88 | total=self._get_seconds_until(timer['attributes']['finishes_at']), 89 | icon=icon, 90 | ) 91 | else: 92 | obj = self.timers.get(timer_id) 93 | if obj is None: 94 | self.log(f"Received event {event_name} for unknown timer {timer_id}") 95 | return 96 | if event_name == UlanziTimerDisplay.TIMER_RESTARTED: 97 | obj.remaining = self._get_seconds_until(timer['attributes']['finishes_at']) + 1 98 | obj.status = 'running' 99 | elif event_name == UlanziTimerDisplay.TIMER_FINISHED: 100 | del self.timers[timer_id] 101 | timer_name = timer['attributes']['friendly_name'] 102 | self.send_notification(f"{timer_name} ist fertig!", icon=icon) 103 | elif event_name == UlanziTimerDisplay.TIMER_CANCELLED: 104 | del self.timers[timer_id] 105 | elif event_name == UlanziTimerDisplay.TIMER_PAUSED: 106 | obj.status = 'paused' 107 | 108 | if not self.timers: 109 | self.delete_app() 110 | self.cancel_timer(self.tick_timer_handle) 111 | self.tick_timer_handle = None 112 | 113 | elif self.tick_timer_handle is None: 114 | self.tick_timer_handle = self.run_every(self.tick, 'now', 1) 115 | 116 | def tick(self, *args, **kwargs): 117 | app_pages = [] 118 | for timer in self.timers.values(): 119 | if timer.status == 'running': 120 | timer.remaining -= 1 121 | app_pages.append(timer.get_output()) 122 | 123 | self.call_service('mqtt/publish', topic=self.mqtt_app_topic, payload=json.dumps(app_pages)) 124 | -------------------------------------------------------------------------------- /uweather.py: -------------------------------------------------------------------------------- 1 | import random 2 | import json 3 | from ulanzi import UlanziApp 4 | 5 | """ 6 | CAUTION 7 | 8 | This module is not async, but needs data obtained from the 9 | weather/get_forecasts service call to Homeassistant. 10 | 11 | If something does not work, check if you have the following 12 | option set in your appdaemon.yaml in the plugins/hass section: 13 | 14 | return_result = true 15 | 16 | """ 17 | 18 | ICON_MAP = { 19 | 'rainy': '72', 20 | 'clear-night': '2285', 21 | # 'cloudy': '2283', 22 | 'cloudy': '53384_', 23 | 'fog': '12196', 24 | 'hail': '2441', 25 | 'lightning': '10341', 26 | 'lightning-rainy': '49299', 27 | 'partlycloudy': 'partlycloudy', 28 | 'pouring': '72', 29 | 'snowy': '2289', 30 | 'snowy-rainy': '49301', 31 | 'sunny': '11201', 32 | 'windy': '55032', 33 | 'windy-variant': '55032', 34 | 'exceptional': '45123', 35 | } 36 | 37 | 38 | class UlanziWeather(UlanziApp): 39 | 40 | def initialize(self): 41 | super().initialize() 42 | try: 43 | self.weather_entity_name = self.args['weather_entity'] 44 | self.weather_entity = self.get_entity(self.weather_entity_name) 45 | self.current_temp_sensor = self.args.get('current_temp_sensor') 46 | except KeyError as err: 47 | self.error("Failed getting configuration {}".format(err.args[0])) 48 | return 49 | 50 | self.update_app() 51 | self.run_every(self.update_app, 'now', 300) 52 | if self.current_temp_sensor: 53 | self.listen_state(self.update_app, self.current_temp_sensor) 54 | 55 | def real_update_app(self, forecast_obj): 56 | # Get current state and temperature 57 | current = self.weather_entity.get_state(attribute="all") 58 | current_icon = ICON_MAP.get(current['state'], ICON_MAP['exceptional']) 59 | if self.current_temp_sensor and (t := self.get_state(self.current_temp_sensor)) != 'unknown': 60 | current_temp = round(float(t), 1) 61 | else: 62 | current_temp = round(float(current['attributes']['temperature']), 1) 63 | 64 | if self.now_is_between('00:00:00', '18:00:00'): 65 | forecast = forecast_obj['forecast'][0] 66 | else: 67 | forecast = forecast_obj['forecast'][1] 68 | temp_tomorrow_low = forecast['templow'] 69 | temp_tomorrow_hight = forecast['temperature'] 70 | temp_tomorrow = f"{int(round(temp_tomorrow_low))} - {int(round(temp_tomorrow_hight))}" 71 | if temp_tomorrow_hight < 0: 72 | temp_tomorrow = f"{int(round(temp_tomorrow_low))} | {int(round(temp_tomorrow_hight))}" 73 | if len(temp_tomorrow) > 7: 74 | temp_tomorrow = temp_tomorrow.replace(' ', '') 75 | icon_tomorrow = ICON_MAP.get(forecast['condition'], ICON_MAP['exceptional']) 76 | preci_tomorrow = forecast['precipitation_probability'] 77 | 78 | payload = [ 79 | { 80 | 'icon': current_icon, 81 | 'text': f"{current_temp}°", 82 | }, 83 | { 84 | 'icon': icon_tomorrow, 85 | 'text': temp_tomorrow, 86 | 'progress': int(preci_tomorrow), 87 | 'progressC': '#2562c4', 88 | 'progressBC': '#373a40', 89 | }, 90 | ] 91 | self.call_service('mqtt/publish', topic=self.mqtt_app_topic, payload=json.dumps(payload)) 92 | # self.log(f"Updated display: {current_temp} | {temp_tomorrow}") 93 | 94 | def update_app(self, *args, **kwargs): 95 | if not self.enabled: 96 | return 97 | 98 | forecast = self.call_service( 99 | "weather/get_forecasts", 100 | target={"entity_id": self.weather_entity_name}, 101 | service_data={"type": "daily"} 102 | ) 103 | 104 | weather_data = None 105 | try: 106 | weather_data = forecast['result']['response'][self.weather_entity_name] 107 | except KeyError: 108 | import pprint 109 | self.log("Got unexpected service callback with:" + pprint.pformat(data['result'])) 110 | return 111 | self.real_update_app(weather_data) 112 | 113 | def get_app_text(self): 114 | raise NotImplementedError 115 | -------------------------------------------------------------------------------- /uwindow.py: -------------------------------------------------------------------------------- 1 | from ulanzi import UlanziApp 2 | 3 | 4 | class UlanziWindowAlert(UlanziApp): 5 | 6 | def initialize(self): 7 | super().initialize() 8 | self.open_windows = set() 9 | try: 10 | self.windows = self.args['windows'] 11 | except KeyError as err: 12 | self.error("Failed getting configuration {}".format(err.args[0])) 13 | return 14 | 15 | for window in self.windows: 16 | window_entity = window['entity'] 17 | window_name = window['name'] 18 | self.listen_state(self.window_action, window_entity, window_name=window_name) 19 | 20 | self.log(f"Initialized {len(self.windows)} windows") 21 | 22 | def window_action(self, entity, attribute, old, new, kwargs): 23 | window_name = kwargs['window_name'] 24 | if old != new and new == 'on': 25 | self.open_windows.add(window_name) 26 | elif old != new and new == 'off': 27 | try: 28 | self.open_windows.remove(window_name) 29 | except KeyError: 30 | pass 31 | 32 | if len(self.open_windows) > 0: 33 | self.update_app() 34 | else: 35 | self.delete_app() 36 | 37 | def get_app_text(self): 38 | return ", ".join(self.open_windows) --------------------------------------------------------------------------------