diff --git a/README.md b/README.md index 75b6eda9..06339a3d 100644 --- a/README.md +++ b/README.md @@ -250,7 +250,7 @@ Please see [appdaemon.yaml](appdaemon/appdaemon.yaml) as an exmaple. ### Configure your NSPanel in AppDaemon Confiure your NSPanel as you like, you need to edit the `apps.yaml` inside of your Appdaemon config folder. -You can have multiple nspanel sections. +You can have multiple nspanel sections. There are some more exmaples in the appdaemon folder of this repo. ```yaml --- @@ -260,57 +260,62 @@ nspanel-1: config: panelRecvTopic: "tele/tasmota_your_mqtt_topic/RESULT" panelSendTopic: "cmnd/tasmota_your_mqtt_topic/CustomSend" - #model: us-p # uncomment this if you have the us version, see table below for more information - timeoutScreensaver: 20 - #brightnessScreensaver: 10 - brightnessScreensaver: + updateMode: "auto-notify" + sleepTimeout: 20 + #sleepBrightness: 10 + sleepBrightness: - time: "7:00:00" value: 10 - time: "23:00:00" value: 0 - locale: "de_DE" - dateFormatBabel: "full" - timeFormat: "%H:%M" - dateFormat: "%A, %d. %B %Y" # ignored if babel python package is installed - weather: weather.example - pages: + locale: "de_DE" # used for translations in translations.py and for localized date if babel python package is installed + screensaver: + entity: weather.k3ll3r + cards: - type: cardEntities - heading: Example Page 1 - items: - - cover.example_cover - - switch.example_switch - - input_boolean.example_input_boolean - - sensor.example_sensor + entities: + - entity: switch.example_item + name: NameOverride + icon: lightbulb + - entity: light.example_item + - entity: cover.example_item + - entity: input_boolean.example_item + title: Example Entities 1 - type: cardEntities - heading: Example Page 2 - items: - - button.example_button - - input_button.example_input_button - - light.light_example - - delete # (read this as 'empty') + entities: + - entity: switch.example_item + - entity: delete + - entity: cover.example_item + - entity: input_boolean.example_item + title: Example Entities 2 - type: cardEntities - heading: Example Page 3 - items: - - scene.example_scene - - delete - - delete - - delete + entities: + - entity: binary_sensor.example_item + - entity: sensor.example_item + - entity: button.example_item + - entity: number.example_item + title: Example Entities 3 + - type: cardEntities + entities: + - entity: scenes.example_item + - entity: script.example_item + - entity: button.example_item + - entity: input_button.example_item + title: Example Entities 4 - type: cardGrid - heading: Example Page 4 - items: - - light.light_example - - button.example_button - - cover.example_cover - - scene.example_scene - - switch.example_switch - - delete + entities: + - entity: light.example_item + - entity: switch.example_item + - entity: delete + - entity: button.example_item + - entity: cover.example_item + title: Exmaple Gird - type: cardThermo - heading: Exmaple Thermostat - item: climate.example_climate + entity: climate.example_item - type: cardMedia - item: media_player.spotify_user + entity: media_player.example_item - type: cardAlarm - item: alarm_control_panel.alarmo + entity: alarm_control_panel.alarmo ``` key | optional | type | default | description @@ -327,13 +332,31 @@ key | optional | type | default | description `panelSendTopic` | False | string | `cmnd/tasmota_your_mqtt_topic/CustomSend` | The mqtt topic used to send messages. `updateMode` | True | string | `auto-notify` | Update Mode; Possible values: "auto", "auto-notify", "manual" `model` | True | string | `eu` | Model; Possible values: "eu", "us-l" and "us-p" -`timeoutScreensaver` | True | integer | `20` | Timeout for the screen to enter screensaver, to disable screensaver use 0 -`brightnessScreensaver` | True | integer/complex | `20` | Brightness for the screen to enter screensaver, see example below for complex/scheduled config. -`brightnessScreensaverTracking` | True | string | None | Forces screensaver brightness to 0 in case entity state is not_home, can be a group, person or device_tracker entity. +`sleepTimeout` | True | integer | `20` | Timeout for the screen to enter screensaver, to disable screensaver use 0 +`sleepBrightness` | True | integer/complex | `20` | Brightness for the screen to enter screensaver, see example below for complex/scheduled config. +`sleepTracking` | True | string | None | Forces screensaver brightness to 0 in case entity state is not_home, can be a group, person or device_tracker entity. `locale` | True | string | `en_US` | Used by babel to determinante Date format on screensaver, also used for localization. `dateFormatBabel` | True | string | `full` | formatting options on https://babel.pocoo.org/en/latest/dates.html?highlight=name%20of%20day#date-fields `timeFormat` | True | string | `%H:%M` | Time Format on screensaver. Substring after `?` is displayed in a seperate smaller textbox. Useful for 12h time format with AM/PM `"%I:%M ?%p"` `dateFormat` | True | string | `%A, %d. %B %Y` | date format used if babel is not installed +`cards` | False | complex | | configuration for cards that are displayed on panel +`screensaver` | True | complex | | configuration for screensaver +`hiddenCards` | True | complex | | configuration for cards that can be accessed though navigate items + +Possible configuration values for a card in card config: + +key | optional | type | default | description +-- | -- | -- | -- | -- +`type` | False | string | `None` | Used by navigate items +`entities` | False | complex | `None` | contains entities of the card, applys only to cardEntities and cardGrid +`heading` | True | string | `None` | Heading of the Page +`entity` | False | string | `None` | contains the entity of the current card, valid for cardThermo, cardAlarm and cardMedia +`key` | True | string | `None` | Used by navigate items + +Possible configuration values for screensaver config: + +key | optional | type | default | description +-- | -- | -- | -- | -- `weather` | True | string | `weather.example` | weather entity from homeassistant `weatherUnit` | True | string | `celsius` | unit for temperature, valid values are `celsius` or `fahrenheit` `weatherOverrideForecast1` | True | string | `None` | sensor entity from home assistant here to override the first weather forecast item on the screensaver @@ -341,14 +364,15 @@ key | optional | type | default | description `weatherOverrideForecast3` | True | string | `None` | sensor entity from home assistant here to override the third weather forecast item on the screensaver `weatherOverrideForecast4` | True | string | `None` | sensor entity from home assistant here to override the forth weather forecast item on the screensaver `doubleTapToUnlock` | True | boolean | `False` | requires to tap screensaver two times -`pages` | False | complex | | configuration for pages on panel +`alternativeLayout` | True | boolean | `False` | alternative layout with humidity +`key` | True | string | `None` | Used by navigate items -#### Schedule screensaver brightness +#### Schedule sleep brightness It is possible to schedule a brightness change for the screen at specific times. ```yaml - brightnessScreensaver: + sleepBrightness: - time: "7:00:00" value: 10 - time: "23:00:00" @@ -361,17 +385,29 @@ To override Icons or Names of entities you can configure an icon and/or name in Only the icons listed in the [Icon Table](HMI#icons-ids) are useable. ```yaml + entities: + - entity: light.test_item + name: NameOverride + icon: lightbulb +``` + +#### Subpages + +You can configure entities with with the prefix `navigate`, that are navigating to cards, in case it's hidden page, the navigation items will change and the arrow is bringing you back to the current page on the top level. + +```yaml + - entity: navigate.cardGrid_testKey +``` + +will allow you to navigate to a cardGrid page with the configured key testKey + +```yaml + hiddenCards: - type: cardGrid - heading: Lights - items: - - light.wled - - light.schreibtischlampe - - switch.deckenbeleuchtung_hinten: - icon: lightbulb - name: Lampe - - delete - - delete - - type: cardMedia + title: Exmaple Grid + entities: + - entity: light.test_item + key: testKey ``` ## How to update @@ -405,7 +441,6 @@ Since release 1.1 you can update the berry driver directly from the Tasmota Cons `UpdateDriverVersion https://raw.githubusercontent.com/joBr99/nspanel-lovelace-ui/main/tasmota/autoexec.be` - ## FAQ - Frequently Asked Questions ### Flashing of the Display Firmware with FlashNextion doesn't work diff --git a/appdaemon/apps.yaml b/appdaemon/apps.yaml index 3304263f..5fd53fff 100644 --- a/appdaemon/apps.yaml +++ b/appdaemon/apps.yaml @@ -6,59 +6,63 @@ nspanel-1: panelRecvTopic: "tele/tasmota_your_mqtt_topic/RESULT" panelSendTopic: "cmnd/tasmota_your_mqtt_topic/CustomSend" updateMode: "auto-notify" - timeoutScreensaver: 20 - #brightnessScreensaver: 10 - brightnessScreensaver: + sleepTimeout: 20 + #sleepBrightness: 10 + sleepBrightness: - time: "7:00:00" value: 10 - time: "23:00:00" value: 0 - locale: "de_DE" # only used if babel python package is installed - dateFormatBabel: "full" # only used if babel python package is installed - # formatting options on https://babel.pocoo.org/en/latest/dates.html?highlight=name%20of%20day#date-fields - timeFormat: "%H:%M" - dateFormat: "%A, %d. %B %Y" # ignored if babel python package is installed - weather: weather.example - weatherOverrideForecast3: sensor.nas_cpu_perc - weatherOverrideForecast4: - sensor.solar_power_current: # use this for overriding name and icon - name: Sonne - icon: solar-power - pages: + locale: "de_DE" # used for translations in translations.py and for localized date if babel python package is installed + screensaver: + entity: weather.k3ll3r + weatherOverrideForecast4: + entity: sensor.example_item + name: name + icon: lightbulb + alternativeLayout: True + cards: - type: cardEntities - heading: Example Page 1 - items: - - cover.example_cover - - switch.example_switch - - input_boolean.example_input_boolean - - sensor.example_sensor + entities: + - entity: switch.example_item + name: NameOverride + icon: lightbulb + - entity: light.example_item + - entity: cover.example_item + - entity: input_boolean.example_item + title: Example Entities 1 - type: cardEntities - heading: Example Page 2 - items: - - button.example_button - - input_button.example_input_button - - light.light_example - - delete # (read this as 'empty') + entities: + - entity: switch.example_item + - entity: delete + - entity: cover.example_item + - entity: input_boolean.example_item + title: Example Entities 2 - type: cardEntities - heading: Example Page 3 - items: - - scene.example_scene - - delete - - delete - - delete + entities: + - entity: binary_sensor.example_item + - entity: sensor.example_item + - entity: button.example_item + - entity: number.example_item + title: Example Entities 3 + - type: cardEntities + entities: + - entity: scenes.example_item + - entity: script.example_item + - entity: button.example_item + - entity: input_button.example_item + title: Example Entities 4 - type: cardGrid - heading: Example Page 4 - items: - - light.light_example - - button.example_button - - cover.example_cover - - scene.example_scene - - switch.example_switch - - delete + entities: + - entity: light.example_item + - entity: switch.example_item + - entity: delete + - entity: button.example_item + - entity: cover.example_item + title: Exmaple Gird - type: cardThermo - heading: Exmaple Thermostat - item: climate.example_climate + entity: climate.example_item - type: cardMedia - item: media_player.spotify_user + entity: media_player.example_item - type: cardAlarm - item: alarm_control_panel.alarmo + entity: alarm_control_panel.alarmo \ No newline at end of file diff --git a/apps/nspanel-lovelace-ui/luibackend/config.py b/apps/nspanel-lovelace-ui/luibackend/config.py index 79251208..ef841f26 100644 --- a/apps/nspanel-lovelace-ui/luibackend/config.py +++ b/apps/nspanel-lovelace-ui/luibackend/config.py @@ -2,98 +2,41 @@ import logging LOGGER = logging.getLogger(__name__) -class PageNode(object): - def __init__(self, data, parent=None): - self.data = data - self.name = None - self.childs = [] - self.parent = parent - self.pos = None +HA_API = None - if "items" in data: - childs = data.pop("items") - for page in childs: - self.add_child(PageNode(page, self)) +class Entity(object): + def __init__(self, entity_input_config): + self.entityId = entity_input_config.get("entity", "unknown") + self.nameOverride = entity_input_config.get("name") + self.iconOverride = entity_input_config.get("icon") - name = self.data.get("heading", "unkown") if type(self.data) is dict else self.data - ptype = self.data.get("type", "unkown") if type(self.data) is dict else "leaf" - - self.name = f"{ptype}.{name}" if type(self.data) is dict else self.data - self.name = self.name.replace(".","_") - self.name = self.name.replace(",","_") - self.name = self.name.replace(" ","_") - - def add_child(self, obj): - obj.pos = len(self.childs) - self.childs.append(obj) - - def next(self): - if self.parent is not None: - pos = self.pos - length = len(self.parent.childs) - return self.parent.childs[(pos+1)%length] - else: - return self - def prev(self): - if self.parent is not None: - pos = self.pos - length = len(self.parent.childs) - return self.parent.childs[(pos-1)%length] - else: - return self - - def search_page_by_name(self, name): - name = name.replace("navigate.", "") - pages = [] - for i in self.childs: - # compare name of current page - if i.name == name: - pages.append(i) - # current pages has also childs - if len(i.childs) > 0: - pages.extend(i.search_page_by_name(name)) - return pages - - return items - - def dump(self, indent=0): - """dump tree to string""" - tab = ' '*(indent-1) + ' |- ' if indent > 0 else '' - name = self.name - parent = self.parent.name if self.parent is not None else "root" - dumpstring = f"{tab}{self.pos}:{name} -> {parent} \n" - for obj in self.childs: - dumpstring += obj.dump(indent + 1) - return dumpstring +class Card(object): + def __init__(self, card_input_config, pos=None): + self.pos = pos + self.raw_config = card_input_config + self.cardType = card_input_config.get("type", "unknown") + self.title = card_input_config.get("title", "unknown") + self.key = card_input_config.get("key", "unknown") + # for single entity card like climate or media + self.entity = None + if card_input_config.get("entity") is not None: + self.entity = Entity(card_input_config) + # for pages like grid or entities + self.entities = [] + for e in card_input_config.get("entities", []): + self.entities.append(Entity(e)) + self.id = f"{self.cardType}_{self.key}".replace(".","_").replace("~","_").replace(" ","_") + LOGGER.info(f"Created Card {self.cardType} with pos {pos} and id {self.id}") - def get_items(self): - items = [] - for i in self.childs: - if len(i.childs) > 0: - items.append(f"navigate.{i.name}") - else: - items.append(i.data) - return items - - def get_all_item_names(self, recursive=True): - items = [] - # current page - if type(self.data) is dict: - items.append(self.data.get("item", next(iter(self.data)))) + def get_entity_list(self): + entityIds = [] + if self.entity is not None: + entityIds.append(self.entity.entityId) else: - items.append(self.data) - # childs of page - for i in self.childs: - if len(i.childs) > 0: - if recursive: - items.extend(i.get_all_item_names()) - else: - if type(i.data) is dict: - items.append(i.data.get("item", next(iter(i.data)))) - else: - items.append(i.data) - return items - + for e in self.entities: + entityIds.append(e.entityId) + return entityIds + class LuiBackendConfig(object): _DEFAULT_CONFIG = { @@ -101,62 +44,111 @@ class LuiBackendConfig(object): 'panelSendTopic': "cmnd/tasmota_your_mqtt_topic/CustomSend", 'updateMode': "auto-notify", 'model': "eu", - 'timeoutScreensaver': 20, - 'brightnessScreensaver': 20, - 'brightnessScreensaverTracking': None, + 'sleepTimeout': 20, + 'sleepBrightness': 20, + 'sleepTracking': None, 'locale': "en_US", 'timeFormat': "%H:%M", 'dateFormatBabel': "full", 'dateFormat': "%A, %d. %B %Y", - 'weather': 'weather.example', - 'weatherUnit': 'celsius', - 'weatherOverrideForecast1': None, - 'weatherOverrideForecast2': None, - 'weatherOverrideForecast3': None, - 'weatherOverrideForecast4': None, - 'doubleTapToUnlock': False, - 'pages': [{ + 'cards': [{ 'type': 'cardEntities', - 'heading': 'Test Entities 1', - 'items': ['switch.test_item', 'switch.test_item', 'switch.test_item'] - }, { + 'entities': [{ + 'entity': 'switch.test_item', + 'name': 'Test Item' + }, { + 'entity': 'switch.test_item' + }], + 'title': 'Example Entities Page' + }, { 'type': 'cardGrid', - 'heading': 'Test Grid 1', - 'items': ['switch.test_item', 'switch.test_item', 'switch.test_item'] - } - ] + 'entities': [{ + 'entity': 'switch.test_item' + }, { + 'entity': 'switch.test_item' + }, { + 'entity': 'switch.test_item' + } + ], + 'title': 'Example Grid Page' + }, { + 'type': 'climate', + 'entity': 'climate.test_item', + }], + 'screensaver': { + 'type': 'screensaver', + 'entity': 'weather.example', + 'weatherUnit': 'celsius', + 'weatherOverrideForecast1': None, + 'weatherOverrideForecast2': None, + 'weatherOverrideForecast3': None, + 'weatherOverrideForecast4': None, + 'doubleTapToUnlock': False, + 'alternativeLayout': False + }, + 'hiddenCards': [] } - def __init__(self, args=None, check=True): + def __init__(self, ha_api, config_in): + global HA_API + HA_API = ha_api self._config = {} - self._page_config = None - - if args: - self.load(args) + self._config_cards = [] + self._config_screensaver = None + self._config_hidden_cards = [] - if check: - self.check() + self.load(config_in) def load(self, args): for k, v in args.items(): if k in self._DEFAULT_CONFIG: self._config[k] = v LOGGER.info(f"Loaded config: {self._config}") + + # parse cards displayed on panel + pos = 0 + for card in self.get("cards"): + self._config_cards.append(Card(card, pos)) + pos = pos + 1 + # parse screensaver + self._config_screensaver = Card(self.get("screensaver")) - root_page = {"items": self.get("pages"), "type": "internal", "heading": "root"} - self._page_config = PageNode(root_page) - - LOGGER.info(f"Parsed Page config to the following Tree: \n {self._page_config.dump()}") - - def check(self): - return + # parsed hidden pages that can be accessed through navigate + for card in self.get("hiddenCards"): + self._config_hidden_cards.append(Card(card)) def get(self, name): - value = self._config.get(name) - if value is None: - value = self._DEFAULT_CONFIG.get(name) + path = name.split(".") + value = self._config + for p in path: + if value is not None: + value = value.get(p, None) + if value is not None: + return value + # try to get a value from default config + value = self._DEFAULT_CONFIG + for p in path: + if value is not None: + value = value.get(p, None) return value + + def get_all_entity_names(self): + entities = [] + for card in self._config_cards: + entities.extend(card.get_entity_list()) + return entities - def get_root_page(self): - return self._page_config + def getCard(self, pos): + card = self._config_cards[pos%len(self._config_cards)] + return card + def searchCard(self, id): + id = id.replace("navigate.", "") + for card in self._config_cards: + if card.id == id: + return card + if self._config_screensaver.id == id: + return screensaver + for card in self._config_hidden_cards: + if card.id == id: + return card \ No newline at end of file diff --git a/apps/nspanel-lovelace-ui/luibackend/controller.py b/apps/nspanel-lovelace-ui/luibackend/controller.py index df4cb0d3..29f22238 100644 --- a/apps/nspanel-lovelace-ui/luibackend/controller.py +++ b/apps/nspanel-lovelace-ui/luibackend/controller.py @@ -13,8 +13,8 @@ class LuiController(object): self._config = config self._send_mqtt_msg = send_mqtt_msg - # first child of root page (default, after startup) - self._current_page = self._config._page_config.childs[0] + # first card (default, after startup) + self._current_card = self._config.getCard(0) self._pages_gen = LuiPagesGen(ha_api, config, send_mqtt_msg) @@ -36,15 +36,14 @@ class LuiController(object): self.register_callbacks() # register callbacks for each time - if type(self._config.get("brightnessScreensaver")) == list: - for index, timeset in enumerate(self._config.get("brightnessScreensaver")): + if type(self._config.get("sleepBrightness")) == list: + for index, timeset in enumerate(self._config.get("sleepBrightness")): self._ha_api.run_daily(self.update_screensaver_brightness, timeset["time"], value=timeset["value"]) # calculate current brightness self.current_screensaver_brightness = self.calc_current_screensaver_brightness() - # call update_screensaver_brightness on changes of entity configured in brightnessScreensaverTracking - bst = self._config.get("brightnessScreensaverTracking") + bst = self._config.get("sleepTracking") if bst is not None and self._ha_api.entity_exists(bst): self._ha_api.listen_state(self.update_screensaver_brightness_state_callback, entity_id=bst) @@ -55,7 +54,7 @@ class LuiController(object): self._pages_gen.update_date("") # set screensaver timeout - timeout = self._config.get("timeoutScreensaver") + timeout = self._config.get("sleepTimeout") self._send_mqtt_msg(f"timeout~{timeout}") # set current screensaver brightness @@ -69,7 +68,7 @@ class LuiController(object): self.update_screensaver_brightness(kwargs={"value": self.current_screensaver_brightness}) def update_screensaver_brightness(self, kwargs): - bst = self._config.get("brightnessScreensaverTracking") + bst = self._config.get("sleepTracking") brightness = 0 if bst is not None and self._ha_api.entity_exists(bst) and self._ha_api.get_entity(bst).state == "not_home": brightness = 0 @@ -84,10 +83,10 @@ class LuiController(object): def calc_current_screensaver_brightness(self): current_screensaver_brightness = 20 # set brightness of screensaver - if type(self._config.get("brightnessScreensaver")) == int: - current_screensaver_brightness = self._config.get("brightnessScreensaver") - elif type(self._config.get("brightnessScreensaver")) == list: - sorted_timesets = sorted(self._config.get("brightnessScreensaver"), key=lambda d: self._ha_api.parse_time(d['time'])) + if type(self._config.get("sleepBrightness")) == int: + current_screensaver_brightness = self._config.get("sleepBrightness") + elif type(self._config.get("sleepBrightness")) == list: + sorted_timesets = sorted(self._config.get("sleepBrightness"), key=lambda d: self._ha_api.parse_time(d['time'])) # calc current screensaver brightness found_current_dim_value = False for index, timeset in enumerate(sorted_timesets): @@ -103,7 +102,7 @@ class LuiController(object): return current_screensaver_brightness def register_callbacks(self): - items = self._config.get_root_page().get_all_item_names() + items = self._config.get_all_entity_names() LOGGER.debug(f"Registering callbacks for the following items: {items}") for item in items: if self._ha_api.entity_exists(item): @@ -111,12 +110,12 @@ class LuiController(object): def state_change_callback(self, entity, attribute, old, new, kwargs): LOGGER.debug(f"Got callback for: {entity}") - LOGGER.debug(f"Current page has the following items: {self._current_page.get_items()}") - if entity in self._current_page.get_all_item_names(recursive=False): + LOGGER.debug(f"Current page has the following items: {self._current_card.get_entity_list()}") + if entity in self._current_card.get_entity_list(): LOGGER.debug(f"Callback Entity is on current page: {entity}") - self._pages_gen.render_page(self._current_page, send_page_type=False) + self._pages_gen.render_card(self._current_card, send_page_type=False) # send detail page update, just in case - if self._current_page.data.get("type", "unknown") in ["cardGrid", "cardEntities"]: + if self._current_card.cardType in ["cardGrid", "cardEntities"]: if entity.startswith("light"): self._pages_gen.generate_light_detail_page(entity) if entity.startswith("cover"): @@ -134,27 +133,29 @@ class LuiController(object): # internal buttons if entity_id == "screensaver" and button_type == "bExit": if self._config.get("doubleTapToUnlock") and int(value) >= 2: - self._pages_gen.render_page(self._current_page) + self._pages_gen.render_card(self._current_card) elif not self._config.get("doubleTapToUnlock"): - self._pages_gen.render_page(self._current_page) + self._pages_gen.render_card(self._current_card) return if button_type == "sleepReached": self._pages_gen.generate_screensaver_page() return - if button_type == "bExit": - self._pages_gen.render_page(self._current_page) + if button_type in ["bExit", "bUp"]: + self._pages_gen.render_card(self._current_card) if button_type == "bNext": - self._current_page = self._current_page.next() - self._pages_gen.render_page(self._current_page) + card = self._config.getCard(self._current_card.pos+1) + self._current_card = card + self._pages_gen.render_card(card) if button_type == "bPrev": - self._current_page = self._current_page.prev() - self._pages_gen.render_page(self._current_page) + card = self._config.getCard(self._current_card.pos-1) + self._current_card = card + self._pages_gen.render_card(card) elif entity_id == "updateDisplayNoYes" and value == "no": - self._pages_gen.render_page(self._current_page) + self._pages_gen.render_card(self._current_card) # buttons with actions on HA if button_type == "OnOff": @@ -180,8 +181,7 @@ class LuiController(object): if button_type == "button": if entity_id.startswith('navigate'): # internal for navigation to nested pages - self._current_page = self._config.get_root_page().search_page_by_name(entity_id)[0] - self._pages_gen.render_page(self._current_page) + self._pages_gen.render_card(self._config.searchCard(entity_id)) elif entity_id.startswith('scene'): self._ha_api.get_entity(entity_id).call_service("turn_on") elif entity_id.startswith('script'): diff --git a/apps/nspanel-lovelace-ui/luibackend/pages.py b/apps/nspanel-lovelace-ui/luibackend/pages.py index 1692d546..7f6e5f3f 100644 --- a/apps/nspanel-lovelace-ui/luibackend/pages.py +++ b/apps/nspanel-lovelace-ui/luibackend/pages.py @@ -55,19 +55,19 @@ class LuiPagesGen(object): def page_type(self, target_page): self._send_mqtt_msg(f"pageType~{target_page}") - def generate_screensaver_page(self): + def generate_screensaver_page(self, card): self.page_type("screensaver") self.update_screensaver_weather() def update_screensaver_weather(self): global babel_spec - we_name = self._config.get("weather") - unit = self._config.get("weatherUnit") + we_name = self._config._config_screensaver.entity.entityId + unit = self._config._config_screensaver.raw_config.get("weatherUnit", "celsius") if self._ha_api.entity_exists(we_name): we = self._ha_api.get_entity(we_name) else: - LOGGER.error("Skipping Weather Update, entity not found") + LOGGER.error("Skipping Weather Update, entity {we_name} not found") return icon_cur = get_icon_id_ha("weather", state=we.state) @@ -75,7 +75,7 @@ class LuiPagesGen(object): weather_res = "" for i in range(1,5): - wOF = self._config.get(f"weatherOverrideForecast{i}") + wOF = self._config._config_screensaver.raw_config.get(f"weatherOverrideForecast{i}") if wOF is None: up = we.attributes.forecast[i-1]['datetime'] up = datetime.datetime.fromisoformat(up) @@ -87,91 +87,86 @@ class LuiPagesGen(object): down = convert_temperature(we.attributes.forecast[i-1]['temperature'], unit) else: LOGGER.info(f"Forecast {i} is overriden with {wOF}") - icon = None - name = None - if type(wOF) is dict: - icon = next(iter(wOF.items()))[1].get('icon') - name = next(iter(wOF.items()))[1].get('name') - wOF = next(iter(wOF.items()))[0] - entity = self._ha_api.get_entity(wOF) + icon = wOF.get("icon") + name = wOF.get("name") + entity = self._ha_api.get_entity(wOF.get("entity")) up = name if name is not None else entity.attributes.friendly_name icon = get_icon_id_ha("sensor", state=entity.state, device_class=entity.attributes.get("device_class", ""), overwrite=icon) unit_of_measurement = entity.attributes.get("unit_of_measurement", "") down = f"{entity.state} {unit_of_measurement}" weather_res+=f"~{up}~{icon}~{down}" - self._send_mqtt_msg(f"weatherUpdate~{icon_cur}~{text_cur}{weather_res}") + altLayout = "" + if self._config._config_screensaver.raw_config.get("alternativeLayout", False) is True: + altLayout = f"~26~{we.attributes.humidity} %" - def generate_entities_item(self, item): - icon = None - name = None - if type(item) is dict: - icon = next(iter(item.items()))[1].get('icon') - name = next(iter(item.items()))[1].get('name') - item = next(iter(item.items()))[0] - # type of the item is the string before the "." in the item name - item_type = item.split(".")[0] - LOGGER.debug(f"Generating item command for {item} with type {item_type}",) - # Internal Entities - if item_type == "delete": - return f"~{item_type}~~~~~" - if item_type == "navigate": - page_search = self._config.get_root_page().search_page_by_name(item) - if len(page_search) > 0: - page_data = page_search[0].data - if name is None: - name = page_data.get("heading") + self._send_mqtt_msg(f"weatherUpdate~{icon_cur}~{text_cur}{weather_res}{altLayout}") + + def generate_entities_item(self, entity): + entityId = entity.entityId + icon = entity.iconOverride + name = entity.nameOverride + # type of the item is the string before the "." in the entityId + entityType = entityId.split(".")[0] + + LOGGER.debug(f"Generating item for {entityId} with type {entityType}",) + # Internal types + if entityType == "delete": + return f"~{entityType}~~~~~" + if entityType == "navigate": + page_search_res = self._config.searchCard(entityId) + if page_search_res is not None: + name = page_search_res.title text = get_translation(self._locale,"PRESS") - icon_id = get_icon_id(icon) if icon is not None else get_icon_id(page_data.get("icon", "gesture-tap-button")) - return f"~button~{item}~{icon_id}~17299~{name}~{text}" + icon_id = get_icon_id(icon) if icon is not None else get_icon_id("gesture-tap-button") + return f"~button~{entityId}~{icon_id}~17299~{name}~{text}" else: - return f"~text~{item}~{get_icon_id('alert-circle-outline')}~17299~page not found~" - if not self._ha_api.entity_exists(item): - return f"~text~{item}~{get_icon_id('alert-circle-outline')}~17299~Not found check~ apps.yaml" + return f"~text~{entityId}~{get_icon_id('alert-circle-outline')}~17299~page not found~" + if not self._ha_api.entity_exists(entityId): + return f"~text~{entityId}~{get_icon_id('alert-circle-outline')}~17299~Not found check~ apps.yaml" # HA Entities - entity = self._ha_api.get_entity(item) + entity = self._ha_api.get_entity(entityId) name = name if name is not None else entity.attributes.friendly_name - if item_type == "cover": + if entityType == "cover": icon_id = get_icon_id_ha("cover", state=entity.state, overwrite=icon) - return f"~shutter~{item}~{icon_id}~17299~{name}~" - if item_type in "light": + return f"~shutter~{entityId}~{icon_id}~17299~{name}~" + if entityType in "light": switch_val = 1 if entity.state == "on" else 0 icon_color = self.get_entity_color(entity) icon_id = get_icon_id_ha("light", overwrite=icon) - return f"~{item_type}~{item}~{icon_id}~{icon_color}~{name}~{switch_val}" - if item_type in ["switch", "input_boolean"]: + return f"~{entityType}~{entityId}~{icon_id}~{icon_color}~{name}~{switch_val}" + if entityType in ["switch", "input_boolean"]: switch_val = 1 if entity.state == "on" else 0 icon_color = self.get_entity_color(entity) - icon_id = get_icon_id_ha(item_type, state=entity.state, overwrite=icon) - return f"~switch~{item}~{icon_id}~{icon_color}~{name}~{switch_val}" - if item_type in ["sensor", "binary_sensor"]: + icon_id = get_icon_id_ha(entityType, state=entity.state, overwrite=icon) + return f"~switch~{entityId}~{icon_id}~{icon_color}~{name}~{switch_val}" + if entityType in ["sensor", "binary_sensor"]: device_class = entity.attributes.get("device_class", "") icon_id = get_icon_id_ha("sensor", state=entity.state, device_class=device_class, overwrite=icon) unit_of_measurement = entity.attributes.get("unit_of_measurement", "") value = entity.state + " " + unit_of_measurement icon_color = self.get_entity_color(entity) - return f"~text~{item}~{icon_id}~{icon_color}~{name}~{value}" - if item_type in ["button", "input_button"]: + return f"~text~{entityId}~{icon_id}~{icon_color}~{name}~{value}" + if entityType in ["button", "input_button"]: icon_id = get_icon_id_ha("button", overwrite=icon) text = get_translation(self._locale,"PRESS") - return f"~button~{item}~{icon_id}~17299~{name}~{text}" - if item_type == "scene": + return f"~button~{entityId}~{icon_id}~17299~{name}~{text}" + if entityType == "scene": icon_id = get_icon_id_ha("scene", overwrite=icon) text = get_translation(self._locale,"ACTIVATE") - return f"~button~{item}~{icon_id}~17299~{name}~{text}" - if item_type == "script": + return f"~button~{entityId}~{icon_id}~17299~{name}~{text}" + if entityType == "script": icon_id = get_icon_id_ha("script", overwrite=icon) text = get_translation(self._locale,"run") - return f"~button~{item}~{icon_id}~17299~{name}~{text}" - if item_type == "number": + return f"~button~{entityId}~{icon_id}~17299~{name}~{text}" + if entityType == "number": icon_id = get_icon_id_ha("number", overwrite=icon) min_v = entity.attributes.get("min", 0) max_v = entity.attributes.get("max", 100) - return f"~number~{item}~{icon_id}~17299~{name}~{entity.state}|{min_v}|{max_v}" + return f"~number~{entityId}~{icon_id}~17299~{name}~{entity.state}|{min_v}|{max_v}" - def generate_entities_page(self, heading, items): - navigation = "" + def generate_entities_page(self, navigation, heading, items): command = f"entityUpd~{heading}~{navigation}" # Get items and construct cmd string for item in items: @@ -180,7 +175,8 @@ class LuiPagesGen(object): - def generate_thermo_page(self, item): + def generate_thermo_page(self, navigation, entity): + item = entity.entityId if not self._ha_api.entity_exists(item): command = f"entityUpd~{item}~Not found~220~220~Not found~150~300~5" else: @@ -237,7 +233,8 @@ class LuiPagesGen(object): command = f"entityUpd~{heading}~~{item}~{current_temp}~{dest_temp}~{status}~{min_temp}~{max_temp}~{step_temp}{icon_res}" self._send_mqtt_msg(command) - def generate_media_page(self, item): + def generate_media_page(self, navigation, entity): + item = entity.entityId if not self._ha_api.entity_exists(item): command = f"entityUpd~|{item}|Not found|{get_icon_id('alert-circle-outline')}|Please check your|apps.yaml in AppDaemon|50|{get_icon_id('alert-circle-outline')}" else: @@ -270,7 +267,8 @@ class LuiPagesGen(object): command = f"entityUpd~|{heading}||{item}|{icon}|{title}|{author}|{volume}|{iconplaypause}|{source}|{speakerlist[:200]}|{onoffbutton}" self._send_mqtt_msg(command) - def generate_alarm_page(self, item): + def generate_alarm_page(self, navigation, entity): + item = entity.entityId if not self._ha_api.entity_exists(item): command = f"entityUpd~{item}~Not found~Not found~Check your~Check your~apps.~apps.~yaml~yaml~0~~0" else: @@ -330,23 +328,29 @@ class LuiPagesGen(object): command = f"entityUpd~{item}~{navigation}{arm_buttons}~{icon}~{color}~{numpad}~{flashing}" self._send_mqtt_msg(command) - def render_page(self, page, send_page_type=True): - config = page.data - page_type = config["type"] - LOGGER.info(f"Started rendering of page {page.pos} with type {page_type}") + def render_card(self, card, send_page_type=True): + LOGGER.info(f"Started rendering of page {card.pos} with type {card.cardType}") + if len(self._config._config_cards) == 1: + navigation = "0|0" + else: + navigation = "1|1" + if card.pos is None: + navigation = "2|0" # Switch to page if send_page_type: - self.page_type(page_type) - if page_type in ["cardEntities", "cardGrid"]: - heading = config.get("heading", "unknown") - self.generate_entities_page(heading, page.get_items()) + self.page_type(card.cardType) + if card.cardType in ["cardEntities", "cardGrid"]: + self.generate_entities_page(navigation, card.title, card.entities) return - if page_type == "cardThermo": - self.generate_thermo_page(page.data.get("item")) - if page_type == "cardMedia": - self.generate_media_page(page.data.get("item")) - if page_type == "cardAlarm": - self.generate_alarm_page(page.data.get("item")) + if card.cardType == "cardThermo": + self.generate_thermo_page(navigation, card.entity) + if card.cardType == "cardMedia": + self.generate_media_page(navigation, card.entity) + if card.cardType == "cardAlarm": + self.generate_alarm_page(navigation, card.entity) + + if card.cardType == "screensaver": + self.generate_screensaver_page(card) def generate_light_detail_page(self, entity): @@ -380,7 +384,7 @@ class LuiPagesGen(object): def generate_shutter_detail_page(self, entity): entity = self._ha_api.get_entity(entity) pos = 100-int(entity.attributes.get("current_position", 50)) - self._send_mqtt_msg(f"entityUpdateDetail,{pos}") + self._send_mqtt_msg(f"entityUpdateDetail~{pos}") def send_message_page(self, id, heading, msg, b1, b2): self._send_mqtt_msg(f"pageType~popupNotify") diff --git a/apps/nspanel-lovelace-ui/nspanel-lovelace-ui.py b/apps/nspanel-lovelace-ui/nspanel-lovelace-ui.py index 3e31b718..ba2e3ae0 100644 --- a/apps/nspanel-lovelace-ui/nspanel-lovelace-ui.py +++ b/apps/nspanel-lovelace-ui/nspanel-lovelace-ui.py @@ -10,7 +10,6 @@ from luibackend.updater import Updater LOGGER = logging.getLogger(__name__) - class AppDaemonLoggingHandler(logging.Handler): def __init__(self, app): super().__init__() @@ -50,7 +49,7 @@ class NsPanelLovelaceUIManager(hass.Hass): def initialize(self): LOGGER.info('Starting') mqtt_api = self._mqtt_api = self.get_plugin_api("MQTT") - cfg = self._cfg = LuiBackendConfig(self.args["config"]) + cfg = self._cfg = LuiBackendConfig(self, self.args["config"]) topic_send = cfg.get("panelSendTopic") def send_mqtt_msg(msg, topic=None): @@ -63,7 +62,7 @@ class NsPanelLovelaceUIManager(hass.Hass): mqtt_api.mqtt_publish(topic_send.replace("CustomSend", "GetDriverVersion"), "x") controller = LuiController(self, cfg, send_mqtt_msg) - + desired_display_firmware_version = 26 version = "v2.4.0" @@ -77,7 +76,6 @@ class NsPanelLovelaceUIManager(hass.Hass): else: # eu version desired_display_firmware_url = f"http://nspanel.pky.eu/lovelace-ui/github/nspanel-{version}.tft" - desired_tasmota_driver_version = 3 desired_tasmota_driver_url = "https://raw.githubusercontent.com/joBr99/nspanel-lovelace-ui/main/tasmota/autoexec.be" diff --git a/info.md b/info.md index ac599812..1486e7c0 100644 --- a/info.md +++ b/info.md @@ -2,72 +2,4 @@ Checkout [README](https://github.com/joBr99/nspanel-lovelace-ui/blob/main/README.md) for detailed Instructions. -### App Configuration - -```yaml ---- -nspanel-1: - module: nspanel-lovelace-ui - class: NsPanelLovelaceUIManager - config: - panelRecvTopic: "tele/tasmota_your_mqtt_topic/RESULT" - panelSendTopic: "cmnd/tasmota_your_mqtt_topic/CustomSend" - updateMode: "auto-notify" - timeoutScreensaver: 20 - #brightnessScreensaver: 10 - brightnessScreensaver: - - time: "7:00:00" - value: 10 - - time: "23:00:00" - value: 0 - locale: "de_DE" # only used if babel python package is installed - dateFormatBabel: "full" # only used if babel python package is installed - # formatting options on https://babel.pocoo.org/en/latest/dates.html?highlight=name%20of%20day#date-fields - timeFormat: "%H:%M" - dateFormat: "%A, %d. %B %Y" # ignored if babel python package is installed - weather: weather.example - pages: - - type: cardEntities - heading: Example Page 1 - items: - - cover.example_cover - - switch.example_switch - - input_boolean.example_input_boolean - - sensor.example_sensor - - type: cardEntities - heading: Example Page 2 - items: - - button.example_button - - input_button.example_input_button - - light.light_example - - delete # (read this as 'empty') - - type: cardEntities - heading: Example Page 3 - items: - - scene.example_scene - - delete - - delete - - delete - - type: cardGrid - heading: Example Page 4 - items: - - light.light_example - - button.example_button - - cover.example_cover - - scene.example_scene - - switch.example_switch - - delete - - type: cardThermo - heading: Exmaple Thermostat - item: climate.example_climate - - type: cardMedia - item: media_player.spotify_user - - type: cardAlarm - item: alarm_control_panel.alarmo -``` - -key | optional | type | default | description --- | -- | -- | -- | -- -`module` | False | string | | The module name of the app. -`class` | False | string | | The name of the Class. -`config` | False | complex | | Config/Mapping between Homeassistant and your NsPanel +![screens](doc-pics/screens.png) \ No newline at end of file