Compare commits

..

32 Commits

Author SHA1 Message Date
Johannes
d745d63def Update copyright year to 2026 2026-02-22 01:20:16 +01:00
Johannes Braun
101e69bbde add migration page 2026-02-22 01:15:08 +01:00
Johannes Braun
d55a63b07c fix css 2026-02-22 01:08:16 +01:00
Johannes Braun
52a041521d fix3 2026-02-22 01:01:31 +01:00
Johannes Braun
f7fa1653c6 fix2 2026-02-22 00:57:18 +01:00
Johannes Braun
b925ea8a2d fix 2026-02-22 00:55:20 +01:00
Johannes Braun
da095b1591 migration mkdocs 2026-02-22 00:53:27 +01:00
Johannes Braun
ca6e271a54 add documentation for standalone version 2026-02-22 00:45:35 +01:00
Johannes
698ad7c771 Update .gitignore to include stuff for macos 2026-02-22 00:44:10 +01:00
Johannes
b0f40a1a87 Bump version to 4.7.85 2026-02-22 00:05:12 +01:00
Johannes
2cf7bcb63e fix loglevel 2026-02-22 00:04:34 +01:00
Johannes
05d9ff52ca Update project maintenance badge year to 2026 2026-02-21 19:37:24 +01:00
Johannes Braun
bb79cd8f85 read loglevel from env 2026-02-21 19:31:31 +01:00
Johannes Braun
00f7119cd2 added info logging for actions from panels 2026-02-21 18:52:54 +01:00
Johannes Braun
8e6f923839 bump version 2026-02-21 18:44:56 +01:00
Johannes Braun
cf396b0259 added error handling and improved logging 2026-02-21 18:44:23 +01:00
Johannes
42c27a3794 Bump version to 4.7.81 2026-02-21 14:42:01 +01:00
Johannes
c65f1935e5 Enhance button press logging with resolved entity ID
Log the resolved entity ID along with the button press event details.
2026-02-21 14:38:37 +01:00
Armilar
17284d83ca v5.1.1.4 - Update print statement from 'Hello' to 'Goodbye'
see Release Notes from 5.0.2.1 no 5.1.1.4
2026-02-17 23:56:37 +01:00
Armilar
2853073a59 Merge pull request #1421 from lubepi/patch-1
Fix: Display waking from screensaver on entity changes after thermostat interaction
2026-02-17 23:51:23 +01:00
Armilar
8de034adf9 Update script version to v5.1.1.4 2026-02-17 23:47:57 +01:00
Armilar
330fa9bfd4 Refactor power subscription handling in NSPanelTs
Refactor subscribePowerSubscriptions to use a subscription key for better management of subscriptions.
2026-02-17 23:43:00 +01:00
lubepi
ec971f5f3e Fix: Display waking from screensaver on entity changes after thermostat interaction
The subscribePowerSubscriptions() function was creating subscriptions via on()
without storing the subscription ID in the subscriptions object. This caused
UnsubscribeWatcher() to be unable to remove the subscription when the screensaver
activated, leaving it active and triggering GeneratePage() on any entity change.

Changes:
- Store subscription in subscriptions object with key 'power_<id>.ACTUAL'
- Add hasOwnProperty check to prevent duplicate subscriptions
2026-02-11 19:10:24 +01:00
dependabot[bot]
41f4062ab8 Bump docker/login-action from 3.6.0 to 3.7.0 (#1418)
Bumps [docker/login-action](https://github.com/docker/login-action) from 3.6.0 to 3.7.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v3.6.0...v3.7.0)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 3.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-02 17:24:35 +01:00
dependabot[bot]
f3fffe7b70 Bump github/codeql-action from 3 to 4 (#1411)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3 to 4.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-30 13:55:10 +01:00
dependabot[bot]
c4b6a8bd8a Bump frenck/action-addon-linter from 2.18 to 2.21 (#1410)
Bumps [frenck/action-addon-linter](https://github.com/frenck/action-addon-linter) from 2.18 to 2.21.
- [Release notes](https://github.com/frenck/action-addon-linter/releases)
- [Commits](https://github.com/frenck/action-addon-linter/compare/v2.18...v2.21)

---
updated-dependencies:
- dependency-name: frenck/action-addon-linter
  dependency-version: '2.21'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-30 13:54:58 +01:00
dependabot[bot]
81d876b53b Bump actions/checkout from 5 to 6 (#1413)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-30 13:54:20 +01:00
Thomas
d15cb218ce Merge pull request #1414 from tt-tom17/main
fix slider and volume
2025-12-29 16:56:05 +01:00
tt-tom17
114f630b8a fix slider and volume 2025-12-29 16:09:02 +01:00
tt-tom17
53b627be88 fix Mute 2025-12-29 16:06:54 +01:00
tt-tom17
f2e1a7263d fix slider and volume 2025-12-29 15:52:34 +01:00
Armilar
1e2f89ed1d Update TypeScript version and fix slider functionality 2025-12-29 13:56:43 +01:00
34 changed files with 1922 additions and 382 deletions

View File

@@ -21,7 +21,7 @@ jobs:
changed: ${{ steps.changed_addons.outputs.changed }}
steps:
- name: Check out the repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Get changed files
id: changed_files
@@ -68,7 +68,7 @@ jobs:
steps:
- name: Check out repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Get information
id: info
@@ -92,7 +92,7 @@ jobs:
- name: Login to GitHub Container Registry
if: env.BUILD_ARGS != '--test'
uses: docker/login-action@v3.6.0
uses: docker/login-action@v3.7.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}

View File

@@ -43,11 +43,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -58,7 +58,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v3
uses: github/codeql-action/autobuild@v4
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -72,4 +72,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v4

View File

@@ -1,28 +0,0 @@
name: docs-ci
on:
workflow_dispatch:
push:
branches:
- dev
paths:
- docs/*
- .github/workflows/docs.yml
- mkdocs.yml
- HMI/README.md
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: actions/setup-python@v6
with:
python-version: 3.x
- run: pip install mkdocs-material mkdocs-video markdown-include mike
- run: cp HMI/README.md docs/hmi-serial-protocol.md
- run: git config --global user.name Docs deploy
- run: git config --global user.email docs@dummy.bot.com
- run: mike deploy --push --update-aliases dev

View File

@@ -6,24 +6,51 @@ on:
branches:
- main
paths:
- docs/*
- docs/**
- docs-standalone/**
- .github/workflows/docs-release.yml
- mkdocs.yml
- docs-standalone/mkdocs.yml
- HMI/README.md
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/setup-python@v6
with:
python-version: 3.x
- run: pip install mkdocs-material mkdocs-video markdown-include mike
- run: pip install zensical
- run: cp HMI/README.md docs/hmi-serial-protocol.md
- run: git config --global user.name Docs deploy
- run: git config --global user.email docs@dummy.bot.com
- run: mike set-default stable
- run: mike deploy --push --update-aliases stable
- run: zensical build --config-file mkdocs.yml
- run: mv site _site_main
- run: zensical build --config-file docs-standalone/mkdocs.yml
- run: mkdir -p _site/standalone _site/stable
- run: cp -a _site_main/. _site/
- run: cp -a _site_main/. _site/stable/
- run: |
if [ -d site-standalone ]; then
cp -a site-standalone/. _site/standalone/
elif [ -d docs-standalone/site ]; then
cp -a docs-standalone/site/. _site/standalone/
elif [ -d site ]; then
cp -a site/. _site/standalone/
elif [ -d docs-standalone/build ]; then
cp -a docs-standalone/build/. _site/standalone/
elif [ -d build ]; then
cp -a build/. _site/standalone/
else
echo "Standalone docs output not found (tried site-standalone, docs-standalone/site, site, docs-standalone/build, build)."
exit 1
fi
- uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_branch: gh-pages
publish_dir: _site
force_orphan: true

View File

@@ -11,7 +11,7 @@ jobs:
name: HACS Action
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v5"
- uses: "actions/checkout@v6"
- name: HACS Action
uses: "hacs/action@main"
with:

View File

@@ -18,7 +18,7 @@ jobs:
gen-ioBroker-localization:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
ref: ${{ github.head_ref }}

View File

@@ -18,7 +18,7 @@ jobs:
addons: ${{ steps.addons.outputs.addons_list }}
steps:
- name: ⤵️ Check out code from GitHub
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: 🔍 Find add-on directories
id: addons
@@ -33,9 +33,9 @@ jobs:
path: ${{ fromJson(needs.find.outputs.addons) }}
steps:
- name: ⤵️ Check out code from GitHub
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: 🚀 Run Home Assistant Add-on Lint
uses: frenck/action-addon-linter@v2.18
uses: frenck/action-addon-linter@v2.21
with:
path: "./${{ matrix.path }}"

View File

@@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
ref: ${{ github.head_ref }}

29
.gitignore vendored
View File

@@ -6,4 +6,31 @@ HMI/Nextion2Text.py
panels.yaml
# don't add Webstorm project stuff
.idea
.idea
# General
.DS_Store
__MACOSX/
.AppleDouble
.LSOverride
Icon[
]
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

View File

@@ -5,7 +5,7 @@ If you like this project consider buying me a pizza 🍕 <a href="https://paypal
[![hacs_badge](https://img.shields.io/badge/HACS-Default-41BDF5.svg)](https://github.com/hacs/integration)
![hacs validation](https://github.com/joBr99/nspanel-lovelace-ui/actions/workflows/hacs-validation.yaml/badge.svg)
[![GitHub Release](https://img.shields.io/github/release/joBr99/nspanel-lovelace-ui.svg)](https://github.com/joBr99/nspanel-lovelace-ui/releases)
![Project Maintenance](https://img.shields.io/maintenance/yes/2024.svg)
![Project Maintenance](https://img.shields.io/maintenance/yes/2026.svg)
[![GitHub Activity](https://img.shields.io/github/commit-activity/y/joBr99/nspanel-lovelace-ui.svg)](https://github.com/joBr99/nspanel-lovelace-ui/commits/main)

View File

@@ -206,11 +206,15 @@ class LuiController(object):
self._pages_gen.generate_timer_detail_page(entity_id, True)
def button_press(self, entity_id, button_type, value):
apis.ha_api.log(f"Button Press Event; entity_id: {entity_id}; button_type: {button_type}; value: {value} ")
#apis.ha_api.log(f"Button Press Event; entity_id: {entity_id}; button_type: {button_type}; value: {value} ")
entity_id_new = entity_id
if entity_id.startswith('uuid'):
entity_config = self._config._config_entites_table.get(entity_id)
if entity_config is not None:
entity_id = entity_config.entityId
entity_id_new = entity_config.entityId
apis.ha_api.log(f"Button Press Event; entity_id: {entity_id}; entity_id_resolved: {entity_id_new}; button_type: {button_type}; value: {value} ")
entity_id = entity_id_new
# internal buttons
if entity_id == "screensaver" and button_type == "bExit":
# get default card if there is one

View File

@@ -0,0 +1,112 @@
/*.md-header__button.md-logo img {
width: unset;
}*/
.md-main__inner {
margin-top: unset;
}
.md-nav__title {
display: none;
}
/*.md-header,*/ .md-footer,
.md-footer-meta {
background-color: #333333;
}
/* Footer contrast fixes for Zensical/Material variants */
:root {
--md-footer-bg-color: #333333;
--md-footer-bg-color--dark: #2b2b2b;
--md-footer-fg-color: #f2f2f2;
--md-footer-fg-color--light: #ffffff;
--md-footer-fg-color--lighter: #ffffff;
}
.md-footer,
.md-footer-meta,
.md-footer * {
color: #f2f2f2;
}
.md-footer a,
.md-footer-meta a,
.md-footer .md-footer__link {
color: #ffffff;
}
.md-footer a:hover,
.md-footer-meta a:hover,
.md-footer .md-footer__link:hover {
color: #d9e7ff;
}
.md-footer .md-icon svg,
.md-footer-meta .md-icon svg {
fill: currentColor;
}
/* Zensical keeps footer content in the inner/meta containers.
Don't hide footer structure, only style it. */
.md-sidebar {
padding-top: 0px;
}
/*.md-sidebar.md-sidebar--primary {
position: unset;
}*/
.md-sidebar.md-sidebar--secondary {
padding-top: 10px;
}
.md-sidebar.md-sidebar--primary .md-sidebar__scrollwrap {
/*overflow-y: unset;*/
padding-right: 1px;
border-right: 1px solid #adadad;
}
.md-sidebar.md-sidebar--primary .md-sidebar__inner {
/*border-right: 1px solid #adadad;*/
padding-bottom: 30px;
}
.md-sidebar.md-sidebar--secondary .md-sidebar__inner {
border-left: 1px solid #adadad;
}
.md-nav__item .md-nav__list {
padding-left: 10px;
}
.md-content {
margin-top: 25px;
/*border-left: 1px solid #adadad;
border-right: 1px solid #adadad;*/
}
.md-top {
display: none;
}
.md-typeset hr {
border-bottom: 1px solid #adadad;
}
.md-typeset h1,
.md-typeset h2,
.md-typeset h3,
.md-typeset h4,
.md-typeset h5 {
font-weight: bold;
}
.md-typeset table:not([class]) td:not(:last-child),
.md-typeset table:not([class]) th:not(:last-child) {
border-right: .05rem solid var(--md-typeset-table-color);
}
ol li::marker {
font-weight: bold;
}

View File

@@ -0,0 +1,137 @@
# Cards
## Supported card types
- `cardEntities`
- `cardGrid`
- `cardQR`
- `cardPower`
- `cardMedia`
- `cardThermo`
- `cardAlarm`
- `cardUnlock`
## Common card keys
key | required | type | description
-- | -- | -- | --
`type` | yes | string | Card type.
`title` | no | string | Card title.
`key` | no | string | Navigation key used by `navigate.<key>`.
## `cardEntities` and `cardGrid`
```yaml
- type: cardEntities
title: Main
key: main
entities:
- entity: light.kitchen
- entity: navigate.settings
icon: mdi:cog
```
- `entities` is required.
- `cardGrid` auto-switches to `cardGrid2` if more than 6 entities are present.
## `cardQR`
```yaml
- type: cardQR
title: Guest WiFi
qrCode: "WIFI:S:myssid;T:WPA;P:mypassword;;"
entities:
- entity: iText.myssid
name: SSID
icon: mdi:wifi
```
Keys:
- `qrCode` optional (default value exists, but set it explicitly)
- supports optional `entity` / `entities`
## `cardPower`
```yaml
- type: cardPower
title: Energy
entities:
- entity: sensor.house_power
- entity: delete
- entity: sensor.solar_power
```
Notes:
- `entities` is required.
- `speed` key is accepted in config but currently not applied by the renderer.
## `cardMedia`
```yaml
- type: cardMedia
title: Living Room
entity: media_player.living_room
entities:
- entity: light.ambient
- entity: switch.tv_bias_light
```
Notes:
- Main media entity must exist (`entity` or first generated entity).
- Additional `entities` are rendered as action buttons on the bottom row.
## `cardThermo`
```yaml
- type: cardThermo
title: Heating
entity: climate.downstairs
supported_modes: ["heat", "off"]
```
Keys:
- `entity` required
- `supported_modes` optional (filters shown HVAC mode buttons)
## `cardAlarm`
```yaml
- type: cardAlarm
title: House Alarm
entity: alarm_control_panel.house
supported_modes: ["arm_home", "arm_away", "arm_night"]
```
Keys:
- `entity` required
- `supported_modes` optional
## `cardUnlock`
```yaml
- type: cardUnlock
title: Admin
pin: 1234
destination: navigate.admin
```
Keys:
- `pin` required
- `destination` required
Typical target in `hiddenCards`:
```yaml
hiddenCards:
- type: cardGrid
key: admin
title: Admin
entities:
- entity: switch.maintenance_mode
```

View File

@@ -0,0 +1,61 @@
# Configuration
The runtime reads one YAML file (default: `./panels.yaml`, add-on mode: `/config/panels.yaml`).
## Top-level keys
key | required | type | default | description
-- | -- | -- | -- | --
`nspanels` | yes | object | none | Map of panel definitions.
`home_assistant_address` | recommended | string | none | Home Assistant base URL. In add-on mode it is auto-filled as `http://supervisor` if missing.
`home_assistant_token` | recommended | string | none | Long-lived token or Supervisor token.
`mqtt_server` | required in MQTT mode | string | from env | MQTT host.
`mqtt_port` | required in MQTT mode | int | from env | MQTT port.
`mqtt_username` | required in MQTT mode | string | from env | MQTT username.
`mqtt_password` | required in MQTT mode | string | from env | MQTT password.
`use_ha_api` | optional | any | absent | If present, MQTT input mode is disabled and HA event mode is used.
`timeZone` | optional | string | `Europe/Berlin` | Global fallback for panel `timeZone`.
`hiddenCards` | optional | list | `[]` | Global fallback for panel `hiddenCards`.
## Panel keys (`nspanels.<name>`)
key | required | type | default | description
-- | -- | -- | -- | --
`panelRecvTopic` | yes | string | none | Receive channel for panel events.
`panelSendTopic` | yes | string | none | Send channel for panel commands.
`locale` | yes | string | none | Locale used for translations and date formatting.
`timeZone` | recommended | string | from top-level `timeZone` | Time zone for clock.
`timeFormat` | yes | string | none | Python `strftime` format.
`dateFormat` | yes | string | none | Babel date format (example: `full`, `medium`).
`model` | optional | string | `eu` | Panel model (`eu`, `us-p`, `us-l`).
`temp_unit` | optional | string | `celsius` | Thermostat card unit (`celsius` or `fahrenheit`).
`sleepTimeout` | optional | int | `20` | Seconds before screensaver.
`sleepBrightness` | optional | int or entity_id | `10` | Screensaver brightness.
`screenBrightness` | optional | int or entity_id | `100` | Active-screen brightness.
`sleepTracking` | optional | entity_id | none | Forces sleep brightness to 0 when entity state matches `sleepTrackingZones`.
`sleepTrackingZones` | optional | list | `["not_home", "off"]` | States that trigger forced dimming.
`sleepOverride` | optional | object | none | Override sleep brightness when entity is `on`/`true`/`home`.
`defaultBackgroundColor` | optional | string | `ha-dark` | `ha-dark` or `black`.
`featExperimentalSliders` | optional | int | `0` | Forwarded in dimmode command.
`defaultCard` | optional | string | none | Default card when leaving screensaver (`navigate.<key>`).
`screensaver` | yes | object | none | Screensaver definition.
`cards` | yes | list | none | Top-level cards.
`hiddenCards` | optional | list | `[]` | Hidden cards addressable through `navigate.<key>`.
## Brightness behavior
- Integer values are used directly.
- Entity values read Home Assistant state and cast to number.
- List/schedule style brightness is not supported in this rewrite.
Example:
```yaml
sleepBrightness: input_number.nspanel_sleep
screenBrightness: input_number.nspanel_awake
sleepTracking: person.john
sleepTrackingZones: ["not_home", "off"]
sleepOverride:
entity: light.bedroom
brightness: 30
```

View File

@@ -0,0 +1,45 @@
# Connection Modes
The rewrite supports two panel input/output modes.
## 1) MQTT mode (default)
Enabled when:
- `mqtt_server` is configured
- `use_ha_api` is not present
Behavior:
- Subscribes to every panel `panelRecvTopic`
- Expects JSON payload containing `CustomRecv`
- Publishes commands to `panelSendTopic`
Example receive payload:
```json
{"CustomRecv":"event,startup,54,eu"}
```
## 2) Home Assistant API mode (`use_ha_api`)
Enabled when key `use_ha_api` exists in config.
Behavior:
- Subscribes to HA event `esphome.nspanel.data`
- Routes events by `device_id` (must match configured `panelRecvTopic`)
- Sends panel commands by calling Home Assistant service:
- `<panelSendTopic>_nspanelui_api_call`
Service payload shape:
```yaml
data: "...panel command..."
command: 2
```
## Common to both modes
- Home Assistant websocket connection is used for entity state cache and service calls.
- UI actions (button presses, sliders, mode selects) are translated to Home Assistant service calls.

View File

@@ -0,0 +1,77 @@
# Entities
Entities are used in cards and screensaver lists.
## Entity keys
key | required | type | description
-- | -- | -- | --
`entity` | yes | string | Home Assistant entity id, or internal entity (`navigate.*`, `delete`, `iText.*`).
`name` | no | string | Display name override.
`icon` | no | string or map | Icon override (`mdi:*`), optionally per state.
`color` | no | `[r,g,b]` or map | Color override, optionally per state.
`value` | no | string | Value override.
`font` | no | string | Icon font variant (`small`, `medium`, `medium-icon`, `large`).
`status` | no | string | Extra status entity for `navigate.*` items.
`effectList` | no | list | Custom light effect list for detail popup.
`attribute` | no | string | Weather attribute to display.
`day` | no | int | Weather daily forecast index.
`hour` | no | int | Weather hourly forecast index.
`unit` | no | string | Value suffix.
## Supported Home Assistant domains
- `switch`
- `input_boolean`
- `automation`
- `lock`
- `input_text`
- `input_select`
- `select`
- `light`
- `fan`
- `button`
- `input_button`
- `scene`
- `script`
- `number`
- `input_number`
- `timer`
- `alarm_control_panel`
- `vacuum`
- `media_player`
- `sun`
- `person`
- `climate`
- `cover`
- `sensor`
- `binary_sensor`
- `weather`
## Internal entities
- `navigate.<key>`: Navigate to card with matching `key`.
- `navigate.UP`: Navigate back.
- `delete`: Placeholder/empty slot.
- `iText.<text>`: Static text entry.
## Template-based values
The rewrite supports Home Assistant template rendering for selected fields when prefixed with `ha:`:
- `icon: "ha:{{ ... }}"`
- `color: "ha:{{ ... }}"` (must evaluate to JSON RGB list)
- `value: "ha:{{ ... }}"`
- `qrCode: "ha:{{ ... }}"`
Example:
```yaml
- entity: light.kitchen
icon:
"on": mdi:lightbulb
"off": mdi:lightbulb-outline
color:
"on": [255, 210, 90]
"off": [80, 120, 170]
```

View File

@@ -0,0 +1,55 @@
# Getting Started
## Home Assistant add-on mode
In add-on mode, the container startup script:
- reads MQTT credentials from Home Assistant service discovery
- sets `CONFIG_FILE=/config/panels.yaml`
- creates `/config/panels.yaml` from the bundled example if it does not exist
Relevant files:
- `nspanel-lovelace-ui/rootfs/usr/bin/mqtt-manager/run.sh`
- `nspanel-lovelace-ui/config.yaml`
## Minimal `panels.yaml`
Start with one panel:
```yaml
home_assistant_address: "http://supervisor"
home_assistant_token: "SUPERVISOR_TOKEN_OR_LONG_LIVED_TOKEN"
nspanels:
kitchen:
panelRecvTopic: "tele/tasmota_kitchen/RESULT"
panelSendTopic: "cmnd/tasmota_kitchen/CustomSend"
locale: "en_US"
timeZone: "Europe/Berlin"
timeFormat: "%H:%M"
dateFormat: "full"
screensaver:
entities:
- entity: weather.home
cards:
- type: cardEntities
title: Main
entities:
- entity: light.kitchen
- entity: switch.coffee_machine
```
## Important notes
- `cards` and `screensaver` are required per panel.
- `timeFormat`, `dateFormat`, and `locale` should be set per panel.
- `panelRecvTopic` / `panelSendTopic` are required.
## Running standalone (outside HA add-on)
If you run this container/process outside Supervisor:
- provide `home_assistant_address` and `home_assistant_token` in YAML
- provide MQTT values in YAML (`mqtt_server`, `mqtt_port`, `mqtt_username`, `mqtt_password`) or environment
- set `CONFIG_FILE` if the config is not `./panels.yaml`

View File

@@ -0,0 +1,38 @@
# Overview
This documentation covers the standalone rewrite located in `nspanel-lovelace-ui/`.
It is a Python backend that:
- receives panel input (MQTT mode or Home Assistant API mode)
- reads Home Assistant state through the websocket API
- renders cards and screensaver pages
- sends panel commands back to the device
This docs set is intentionally separate from the AppDaemon docs in `docs/`.
## Rewrite location
- Add-on package: `nspanel-lovelace-ui/`
- Runtime code: `nspanel-lovelace-ui/rootfs/usr/bin/mqtt-manager/`
- Example panel config: `nspanel-lovelace-ui/rootfs/usr/bin/mqtt-manager/panels.yaml.example`
## What is supported
- `cardEntities`
- `cardGrid` (auto-switches to `cardGrid2` when needed)
- `cardQR`
- `cardPower`
- `cardMedia`
- `cardThermo`
- `cardAlarm`
- `cardUnlock`
- screensaver with status icons and weather forecast entities
## Runtime model
1. Load `panels.yaml`.
2. Resolve MQTT and Home Assistant connection settings.
3. Create one thread per panel.
4. Listen for events and state changes.
5. Re-render active pages and detail popups when relevant entities change.

View File

@@ -0,0 +1,120 @@
# Migration from AppDaemon Config
This page explains how to migrate panel configuration from the legacy AppDaemon `apps.yaml` format to the standalone rewrite `panels.yaml` format.
## File and structure changes
Old (AppDaemon):
- panel config lived under `apps.yaml`
- MQTT and Home Assistant base connection config was split across AppDaemon files (`appdaemon.yaml`, plugin config, and app config)
New (rewrite):
- panel config lives in one file: `panels.yaml` (usually `/config/panels.yaml`)
- connection values are read from this file and/or environment variables
## Minimal before/after example
Old AppDaemon (`apps.yaml`):
```yaml
nspanel-1:
module: nspanel-lovelace-ui
class: NsPanelLovelaceUIManager
config:
panelRecvTopic: "tele/tasmota_panel/RESULT"
panelSendTopic: "cmnd/tasmota_panel/CustomSend"
model: eu
locale: en_US
timeFormat: "%H:%M"
```
New rewrite (`panels.yaml`):
```yaml
home_assistant_address: "http://supervisor"
home_assistant_token: "YOUR_TOKEN"
nspanels:
panel-1:
panelRecvTopic: "tele/tasmota_panel/RESULT"
panelSendTopic: "cmnd/tasmota_panel/CustomSend"
model: eu
locale: en_US
timeZone: "Europe/Berlin"
timeFormat: "%H:%M"
dateFormat: "full"
screensaver:
entities:
- entity: weather.home
cards:
- type: cardEntities
title: Main
entities:
- entity: light.kitchen
```
## Key mapping
Legacy AppDaemon key or concept | Standalone rewrite | Notes
-- | -- | --
`module`, `class`, `config` wrapper | removed | Rewrite uses `nspanels.<panel_name>` directly.
`panelRecvTopic` | `panelRecvTopic` | Same meaning.
`panelSendTopic` | `panelSendTopic` | Same meaning.
`model` | `model` | Same meaning (`eu`, `us-p`, `us-l`).
`locale` | `locale` | Same meaning.
`timeFormat` | `timeFormat` | Same meaning.
`timezone` (legacy docs casing) | `timeZone` | Use exact camelCase `timeZone`.
`dateFormatBabel` / `dateFormat` | `dateFormat` | Rewrite expects `dateFormat`.
`cards` | `cards` | Same concept.
`hiddenCards` | `hiddenCards` | Same concept.
`screensaver` | `screensaver` | Same concept; some legacy theme options are not available.
`defaultCard` under screensaver usage | `defaultCard` (panel level) | Use as panel-level key in rewrite.
`temperatureUnit` (card-level legacy usage) | `temp_unit` (panel level) | Rewrite reads panel-level `temp_unit`.
`sleepBrightness` list schedule | not supported | Rewrite supports integer or entity id, not list-based schedules.
`screenBrightness` list schedule | not supported | Rewrite supports integer or entity id, not list-based schedules.
`sleepTracking` | `sleepTracking` | Same concept.
`sleepTrackingZones` | `sleepTrackingZones` | Same concept.
`sleepOverride` | `sleepOverride` | Same concept.
`updateMode` / OTA URL overrides (`displayURL-*`, `berryURL`) | not supported | Rewrite does not implement these legacy update keys.
`theme`, `dateAdditionalTemplate`, `timeAdditionalTemplate` | not supported | Not implemented in rewrite config.
## Connection config differences
In AppDaemon setups, MQTT and Home Assistant connectivity was mostly configured via AppDaemon plugin settings.
In the rewrite, connectivity is resolved directly by the runtime:
- Home Assistant:
- `home_assistant_address`
- `home_assistant_token`
- MQTT (for MQTT mode):
- `mqtt_server`, `mqtt_port`, `mqtt_username`, `mqtt_password`
- Optional mode switch:
- set `use_ha_api` to use Home Assistant event mode instead of MQTT receive mode
## Entity-level differences to watch
Some legacy entity config fields are not implemented in the rewrite parser/renderer:
- `state`, `state_not`, `state_template`
- direct `service.*` action entries with custom `data`
- `action_name`
Supported and commonly used fields in rewrite:
- `entity`, `name`, `icon`, `color`, `value`, `font`
- weather-related: `attribute`, `day`, `hour`, `unit`
- light detail helper: `effectList`
- navigation helper: `status` for `navigate.*` entities
## Migration checklist
1. Create `/config/panels.yaml` from the rewrite example.
2. Move each old app entry (`nspanel-1`, `nspanel-2`, ...) into `nspanels`.
3. Remove `module/class/config` wrappers.
4. Rename `timezone` to `timeZone`.
5. Ensure each panel has `dateFormat`, `timeFormat`, `screensaver`, and `cards`.
6. Replace unsupported scheduled brightness lists with integer/entity-based values.
7. Remove unsupported legacy-only keys listed above.

View File

@@ -0,0 +1,57 @@
# Screensaver
`screensaver` is a required object in each panel config.
## Keys
key | required | type | default | description
-- | -- | -- | -- | --
`type` | no | string | `screensaver` | Layout type (`screensaver` / `screensaver2`).
`entities` | yes* | list | none | Screensaver entities.
`entity` | yes* | string | none | Single-entity shortcut.
`statusIcon1` | no | object | none | Left status icon near date.
`statusIcon2` | no | object | none | Right status icon near date.
`doubleTapToUnlock` | no | bool | `false` | Requires double tap when leaving screensaver.
`sleepTimeout` | no | int | panel `sleepTimeout` | Per-screensaver timeout override.
`*` Provide at least one of `entity` or `entities`.
## Screensaver entities
Screensaver entities use the same entity format as other cards.
For `weather.<entity>` you can also use:
- `attribute` (default `temperature`)
- `day` (daily forecast index)
- `hour` (hourly forecast index)
- `unit` (suffix, default `°C` for temperature-like attributes)
## Example
```yaml
screensaver:
type: screensaver
doubleTapToUnlock: true
sleepTimeout: 30
statusIcon1:
entity: binary_sensor.front_door
icon:
"on": mdi:door-open
"off": mdi:door-closed
font: medium-icon
statusIcon2:
entity: sensor.outdoor_temperature
icon: mdi:thermometer
entities:
- entity: weather.home
attribute: temperature
- entity: weather.home
day: 1
attribute: temperature
- entity: weather.home
day: 2
attribute: temperature
- entity: sensor.indoor_temperature
icon: mdi:home-thermometer
```

View File

@@ -0,0 +1,63 @@
# Troubleshooting
## Config does not load
Symptoms:
- no panel output
- log shows YAML parse error or file missing
Checks:
1. Confirm `CONFIG_FILE` path.
2. Validate YAML syntax.
3. Ensure required per-panel keys exist: `panelRecvTopic`, `panelSendTopic`, `locale`, `timeFormat`, `dateFormat`, `screensaver`, `cards`.
## MQTT not connected
Symptoms:
- log repeats MQTT connection retry
Checks:
1. Verify `mqtt_server`, `mqtt_port`, `mqtt_username`, `mqtt_password`.
2. Verify panel publishes on the same topic as `panelRecvTopic`.
3. Verify payload includes `CustomRecv` JSON key.
## Home Assistant websocket not connected
Symptoms:
- log repeatedly waits for websocket/auth
Checks:
1. Verify `home_assistant_address` and `home_assistant_token`.
2. In add-on mode, verify Supervisor token access is available.
3. Confirm HA is reachable from container network.
## Card does not open or navigate
Checks:
1. For `navigate.<key>`, confirm target card has matching `key`.
2. For `cardUnlock`, confirm `destination` and `pin` are set.
3. Confirm card `type` is one of the implemented types.
## Brightness behaves unexpectedly
Checks:
1. If using entity-based brightness, verify entity state is numeric.
2. Avoid list-based brightness schedules in this rewrite (not supported).
3. Review interaction between `sleepTracking`, `sleepTrackingZones`, and `sleepOverride`.
## Useful logs to look for
- `Config file not found`
- `Error while parsing YAML file`
- `Connected to MQTT Server`
- `Home Assistant auth OK`
- `card type ... not implemented`
- `Not implemented: <button action>`

View File

@@ -0,0 +1,60 @@
site_name: NsPanel Lovelace UI Standalone Docs
site_description: Documentation for the standalone/Home Assistant add-on rewrite in nspanel-lovelace-ui.
site_author: Johannes Braun
site_url: https://jobr99.github.io/nspanel-lovelace-ui/standalone
repo_name: jobr99/nspanel-lovelace-ui
repo_url: https://github.com/jobr99/nspanel-lovelace-ui
edit_uri: ""
copyright: "Copyright © 2026 Johannes Braun"
docs_dir: docs
theme:
name: material
palette:
accent: blue
font:
text: "arial, sans-serif"
code: monospace
features:
- navigation.indexes
- navigation.sections
- navigation.top
- navigation.tracking
- navigation.expand
- search.highlight
- search.share
- search.suggest
extra_css:
- _assets/user.css
markdown_extensions:
- admonition
- def_list
- attr_list
- pymdownx.tilde
- pymdownx.details
- pymdownx.superfences
- pymdownx.magiclink
- toc:
permalink: true
- codehilite:
guess_lang: false
plugins:
- search:
lang: en
nav:
- "Overview": index.md
- "Getting Started": getting-started.md
- "Configuration": configuration.md
- "Migration from AppDaemon": migration-appdaemon.md
- "Screensaver": screensaver.md
- "Cards": cards.md
- "Entities": entities.md
- "Connection Modes": connection-modes.md
- "Troubleshooting": troubleshooting.md

View File

@@ -10,14 +10,46 @@
display: none;
}
/*.md-header,*/ .md-footer {
/*.md-header,*/ .md-footer,
.md-footer-meta {
background-color: #333333;
}
.md-footer__inner.md-grid {
display: none;
/* Footer contrast fixes for Zensical/Material variants */
:root {
--md-footer-bg-color: #333333;
--md-footer-bg-color--dark: #2b2b2b;
--md-footer-fg-color: #f2f2f2;
--md-footer-fg-color--light: #ffffff;
--md-footer-fg-color--lighter: #ffffff;
}
.md-footer,
.md-footer-meta,
.md-footer * {
color: #f2f2f2;
}
.md-footer a,
.md-footer-meta a,
.md-footer .md-footer__link {
color: #ffffff;
}
.md-footer a:hover,
.md-footer-meta a:hover,
.md-footer .md-footer__link:hover {
color: #d9e7ff;
}
.md-footer .md-icon svg,
.md-footer-meta .md-icon svg {
fill: currentColor;
}
/* Zensical keeps footer content in the inner/meta containers.
Don't hide footer structure, only style it. */
.md-sidebar {
padding-top: 0px;
}
@@ -77,4 +109,4 @@
ol li::marker {
font-weight: bold;
}
}

View File

@@ -0,0 +1,75 @@
# Migration to Standalone Rewrite Config
This page compares the legacy AppDaemon `apps.yaml` config with the standalone rewrite `panels.yaml` config.
For the full rewrite docs, including full key descriptions, see:
- [Standalone documentation](https://docs.nspanel.pky.eu/standalone/)
- [Standalone migration page](https://docs.nspanel.pky.eu/standalone/migration-appdaemon/)
## High-level differences
Old AppDaemon version:
- panel config in `apps.yaml` with `module` / `class` / `config`
- connectivity partly configured in AppDaemon plugin config (`appdaemon.yaml`)
Standalone rewrite:
- one runtime config file: `/config/panels.yaml`
- panel definitions under `nspanels`
- Home Assistant and MQTT connection values resolved directly by the rewrite runtime
## Minimal before/after example
Old (`apps.yaml`):
```yaml
nspanel-1:
module: nspanel-lovelace-ui
class: NsPanelLovelaceUIManager
config:
panelRecvTopic: "tele/tasmota_panel/RESULT"
panelSendTopic: "cmnd/tasmota_panel/CustomSend"
model: eu
```
New (`panels.yaml`):
```yaml
home_assistant_address: "http://supervisor"
home_assistant_token: "YOUR_TOKEN"
nspanels:
panel-1:
panelRecvTopic: "tele/tasmota_panel/RESULT"
panelSendTopic: "cmnd/tasmota_panel/CustomSend"
model: eu
locale: en_US
timeZone: "Europe/Berlin"
timeFormat: "%H:%M"
dateFormat: "full"
screensaver:
entities:
- entity: weather.home
cards:
- type: cardEntities
title: Main
entities:
- entity: light.kitchen
```
## Important key changes
Legacy key/concept | Rewrite key/concept | Notes
-- | -- | --
`module`, `class`, `config` wrapper | removed | Rewrite uses `nspanels.<panel_name>` directly.
`timezone` | `timeZone` | Casing changed.
`dateFormatBabel` | `dateFormat` | Use `dateFormat` in rewrite.
`temperatureUnit` (legacy card-level usage) | `temp_unit` (panel-level) | Rewrite reads `temp_unit` from panel settings.
brightness schedule lists | not supported | Rewrite supports integer or entity id for brightness values.
`updateMode` / OTA URL override keys | not supported | Legacy update behavior is not part of rewrite config.
If you are migrating now, use the standalone migration page for the complete mapping:
- [Complete mapping and checklist](https://docs.nspanel.pky.eu/standalone/migration-appdaemon/)

View File

@@ -1,6 +1,6 @@
/*-----------------------------------------------------------------------
TypeScript v5.1.1.2 zur Steuerung des SONOFF NSPanel mit dem ioBroker by @Armilar / @TT-Tom / @ticaki / @Britzelpuf / @Sternmiere / @ravenS0ne
- abgestimmt auf TFT 61 / v5.1.1 / BerryDriver 10 / Tasmota 15.2.0
TypeScript v5.1.1.4 zur Steuerung des SONOFF NSPanel mit dem ioBroker by @Armilar / @TT-Tom / @ticaki / @Britzelpuf / @Sternmiere / @ravenS0ne
- abgestimmt auf TFT 61 / v5.1.1 (v5.1.2 us-p) / BerryDriver 10 / Tasmota 15.2.0
Projekt:
https://github.com/joBr99/nspanel-lovelace-ui/tree/main/ioBroker
@@ -101,7 +101,10 @@ ReleaseNotes:
- 21.11.2025 - v5.1.1.1 Add some LongPress Actions in TFT/HMI v5.1.1 for NSPanel Adapter
- 21.11.2025 - v5.1.1.1 Remove Subscription if .ON and ON_ACTUAL
- 21.12.2025 - v5.1.1.2 Left screensaver unit from ioBroker data point to create a dynamic screensaver (by ernstdaheim-hub)
- 29.12.2025 - v5.1.1.3 Fix popupSlider (Standard-Slider (not cardMedia) with Functionality on popupSlider) / Wrong Pictures in us-p Slider if BG-Color is black (0)
- 29.12.2025 - v5.1.1.4 Refactor power subscription handling in NSPanelTs (#1421 by lubepi)
***************************************************************************************************************
* DE: Für die Erstellung der Aliase durch das Skript, muss in der JavaScript Instanz "setObject" gesetzt sein! *
* EN: In order for the script to create the aliases, “setObject” must be set in the JavaScript instance! *
@@ -216,7 +219,7 @@ Install/Upgrades in Konsole:
TFT EU STABLE Version: FlashNextionAdv0 http://nspanel.de/nspanel-v5.1.1.tft
TFT US-L STABLE Version: FlashNextionAdv0 http://nspanel.de/nspanel-us-l-v5.1.1.tft
TFT US-P STABLE Version: FlashNextionAdv0 http://nspanel.de/nspanel-us-p-v5.1.1.tft
TFT US-P STABLE Version: FlashNextionAdv0 http://nspanel.de/nspanel-us-p-v5.1.2.tft
---------------------------------------------------------------------------------------
*/
@@ -1002,7 +1005,7 @@ export const config: Config = {
// _________________________________ DE: Ab hier keine Konfiguration mehr _____________________________________
// _________________________________ EN: No more configuration from here _____________________________________
const scriptVersion: string = 'v5.1.1.2';
const scriptVersion: string = 'v5.1.1.4';
const tft_version: string = 'v5.1.1';
const desired_display_firmware_version = 61;
const berry_driver_version = 10;
@@ -8849,7 +8852,11 @@ function unsubscribePowerSubscriptions (): void {
* @returns {void}
*/
function subscribePowerSubscriptions (id: string): void {
on({id: id + '.ACTUAL', change: 'ne'}, async function () {
const subscriptionKey = 'power_' + id + '.ACTUAL';
if (subscriptions.hasOwnProperty(subscriptionKey)) {
return;
}
subscriptions[subscriptionKey] = on({id: id + '.ACTUAL', change: 'ne'}, async function () {
(function () {
if (timeoutPower) {
clearTimeout(timeoutPower);
@@ -8862,7 +8869,6 @@ function subscribePowerSubscriptions (id: string): void {
});
}
/**
* @function GeneratePowerPage
* @description Generates a page with power state and energy usage information.
@@ -10188,6 +10194,10 @@ function HandleButtonEvent (words: any): void {
break;
}
}
} else {
let pageItemSlider = findPageItem(id);
let sliderPos = Math.trunc(scale(parseInt(words[4]), 0, 100, pageItemSlider.maxValueLevel ? pageItemSlider.maxValue : 100, pageItemSlider.minValueLevel ? pageItemSlider.minValue : 0));
setIfExists(pageItemSlider.id + '.SET', sliderPos) ? true : setIfExists(id + '.ACTUAL', sliderPos);
}
break;
case 'mode-seek':
@@ -11448,7 +11458,7 @@ function GenerateDetailPage (type: NSPanel.PopupType, optional: NSPanel.mediaOpt
let hSlider2MinVal: number = pageItem.minValue ?? 0;
let hSlider2MaxVal: number = pageItem.maxValue ?? 100;
let hSlider2ZeroVal: number = 0;
let hSlider2CurVal: number = getState(id + '.ACTUAL').val;
let hSlider2CurVal: number = existsState(id + '.ACTUAL') ? getState(id + '.ACTUAL').val : getState(id + '.SET').val;
let hSlider2Step: number = 1;
let hSlider2Visibility: string = "enable";

View File

@@ -1,6 +1,6 @@
/*-----------------------------------------------------------------------
TypeScript v5.0.2.1 zur Steuerung des SONOFF NSPanel mit dem ioBroker by @Armilar / @TT-Tom / @ticaki / @Britzelpuf / @Sternmiere / @ravenS0ne
- abgestimmt auf TFT 59 / v5.0.2 / BerryDriver 10 / Tasmota 15.0.1
TypeScript v5.1.1.4 zur Steuerung des SONOFF NSPanel mit dem ioBroker by @Armilar / @TT-Tom / @ticaki / @Britzelpuf / @Sternmiere / @ravenS0ne
- abgestimmt auf TFT 61 / v5.1.1 (v5.1.2 us-p) / BerryDriver 10 / Tasmota 15.2.0
Projekt:
https://github.com/joBr99/nspanel-lovelace-ui/tree/main/ioBroker
@@ -94,8 +94,17 @@ ReleaseNotes:
- 08.09.2025 - v5.0.0 TFT 59 / 5.0.0 - US-L/US-P Changes in cardMedia, popupInSel, card Grid 1, 2, 3
- 19.09.2025 - v5.0.0.2 Remove Startup Scheedule at 3:30am / Small fix
- 19.10.2025 - v5.0.2.1 TFT 59 / 5.0.2 - EU/US-L/US-P - Fix cardAlarm Icon; Fix Notification in Advanced Screensaver; Fix Dimensions in cardChart/cardLChart
- 12.11.2025 - v5.1.0 TFT 61 / 5.1.0 - Breaking Changes in popupNotify TFT - add 3. Button only for Adapter
- 12.11.2025 - v5.1.0.1 Change Brightsky icon to icon_special
- 15.11.2025 - v5.1.0.2 Add Swiss-Weather-API Adapter
- 18.11.2025 - v5.1.0.3 Fix QR-Code Generation cardQR
- 21.11.2025 - v5.1.1.1 Add some LongPress Actions in TFT/HMI v5.1.1 for NSPanel Adapter
- 21.11.2025 - v5.1.1.1 Remove Subscription if .ON and ON_ACTUAL
- 21.12.2025 - v5.1.1.2 Left screensaver unit from ioBroker data point to create a dynamic screensaver (by ernstdaheim-hub)
- 29.12.2025 - v5.1.1.3 Fix popupSlider (Standard-Slider (not cardMedia) with Functionality on popupSlider) / Wrong Pictures in us-p Slider if BG-Color is black (0)
- 29.12.2025 - v5.1.1.4 Refactor power subscription handling in NSPanelTs (#1421 by lubepi)
***************************************************************************************************************
* DE: Für die Erstellung der Aliase durch das Skript, muss in der JavaScript Instanz "setObject" gesetzt sein! *
* EN: In order for the script to create the aliases, “setObject” must be set in the JavaScript instance! *
@@ -190,9 +199,13 @@ Tasmota-Status0 - (zyklische Ausführung)
Erforderliche Adapter:
Pirate-Weather oder BrightSky oder OpenWeatherMap --> Bei Nutzung der Wetterfunktionen (und zur Icon-Konvertierung) im Screensaver
!!!DasWetter deprecated - Dienst nur noch für ältere Accounts funktional
!!!AccuWeather deprecated - Dienst schaltet Free-Account ab!!!
Bei Nutzung der Wetterfunktionen (und zur Icon-Konvertierung) im Screensaver einen der folgenden Wetter-Adapter:
- Pirate-Weather
- BrightSky
- OpenWeatherMap
- Swiss-Weather-API
- !!!DasWetter deprecated - Dienst nur noch für ältere Accounts funktional
- !!!AccuWeather deprecated - Dienst schaltet Free-Account ab!!!
Alexa2: - Bei Nutzung der dynamischen SpeakerList in der cardMedia
Geräte verwalten - Für Erstellung der Aliase
MQTT-Adapter - Für Kommunikation zwischen Skript und Tasmota
@@ -203,10 +216,10 @@ Install/Upgrades in Konsole:
Tasmota BerryDriver Install: Backlog UrlFetch https://raw.githubusercontent.com/ticaki/ioBroker.nspanel-lovelace-ui/refs/heads/main/tasmota/berry/10/autoexec.be; Restart 1
Tasmota BerryDriver Update: Backlog UpdateDriverVersion https://raw.githubusercontent.com/ticaki/ioBroker.nspanel-lovelace-ui/refs/heads/main/tasmota/berry/10/autoexec.be; Restart 1
TFT EU STABLE Version: FlashNextionAdv0 http://nspanel.de/nspanel-v5.0.2.tft
TFT EU STABLE Version: FlashNextionAdv0 http://nspanel.de/nspanel-v5.1.1.tft
TFT US-L STABLE Version: FlashNextionAdv0 http://nspanel.de/nspanel-us-l-v5.0.2.tft
TFT US-P STABLE Version: FlashNextionAdv0 http://nspanel.de/nspanel-us-p-v5.0.2.tft
TFT US-L STABLE Version: FlashNextionAdv0 http://nspanel.de/nspanel-us-l-v5.1.1.tft
TFT US-P STABLE Version: FlashNextionAdv0 http://nspanel.de/nspanel-us-p-v5.1.2.tft
---------------------------------------------------------------------------------------
*/
@@ -254,8 +267,8 @@ const NSPanel_Alarm_Path = '0_userdata.0.NSPanel.';
/***** 3. Weather adapter Config *****/
// DE: Mögliche Wetteradapter 'pirate-weather.0.' oder 'brightsky.0.' oder 'openweathermap.0.' oder 'daswetter.0.' (deprecated) oder 'accuweather.0.' (deprecated)
// EN: Possible weather adapters 'pirate-weather.0.' or 'brightsky.0.' or 'openweathermap.0.' or 'daswetter.0.' (deprecated) or 'accuweather.0.' (deprecated)
// DE: Mögliche Wetteradapter 'pirate-weather.0.' oder 'brightsky.0.' oder 'openweathermap.0.' oder 'swiss-weather-api.0.' oder 'daswetter.0.' (deprecated) oder 'accuweather.0.' (deprecated)
// EN: Possible weather adapters 'pirate-weather.0.' or 'brightsky.0.' or 'openweathermap.0.' or 'swiss-weather-api.0.' or 'daswetter.0.' (deprecated) or 'accuweather.0.' (deprecated)
const weatherAdapterInstance: string = 'pirate-weather.0.';
// DE: Mögliche Werte: 'Min', 'Max' oder 'MinMax' im Screensaver
@@ -992,9 +1005,9 @@ export const config: Config = {
// _________________________________ DE: Ab hier keine Konfiguration mehr _____________________________________
// _________________________________ EN: No more configuration from here _____________________________________
const scriptVersion: string = 'v5.0.2.1';
const tft_version: string = 'v5.0.2';
const desired_display_firmware_version = 59;
const scriptVersion: string = 'v5.1.1.4';
const tft_version: string = 'v5.1.1';
const desired_display_firmware_version = 61;
const berry_driver_version = 10;
const tasmotaOtaUrl: string = 'http://ota.tasmota.com/tasmota32/release/';
@@ -1111,6 +1124,11 @@ async function CheckConfigParameters () {
log('Weather adapter: << weatherAdapterInstance - ' + weatherAdapterInstance + ' >> is not installed. Please Check Adapter!', 'error');
}
}
if (weatherAdapterInstance.substring(0, weatherAdapterInstance.length - 3) == 'swiss-weather-api') {
if (existsObject(weatherAdapterInstance + 'forecast.days.day0.0000.symbol_code') == false) {
log('Weather adapter: << weatherAdapterInstance - ' + weatherAdapterInstance + ' >> is not installed. Please Check Adapter!', 'error');
}
}
let weatherAdapterInstanceArray: any = weatherAdapterInstance.split('.');
weatherAdapterInstanceNumber = weatherAdapterInstanceArray[1];
@@ -2572,11 +2590,11 @@ async function CreateWeatherAlias () {
if (!existsState(config.weatherEntity + '.ICON') && existsState('brightsky.' + weatherAdapterInstanceNumber + '.current.icon')) {
log('Weather alias for brightsky.' + weatherAdapterInstanceNumber + '. does not exist yet, will be created now', 'info');
setObject(config.weatherEntity, {_id: config.weatherEntity, type: 'channel', common: {role: 'weatherCurrent', name: 'weatherCurrent'}, native: {}});
await createAliasAsync(config.weatherEntity + '.ICON', ('brightsky.' + weatherAdapterInstanceNumber + '.current.icon'), true, {
await createAliasAsync(config.weatherEntity + '.ICON', ('brightsky.' + weatherAdapterInstanceNumber + '.current.icon_special'), true, {
type: 'string',
role: 'value',
name: 'ICON',
alias: {id: 'brightsky.' + weatherAdapterInstanceNumber + '.current.icon'},
alias: {id: 'brightsky.' + weatherAdapterInstanceNumber + '.current.icon_special'},
});
await createAliasAsync(config.weatherEntity + '.TEMP', 'brightsky.' + weatherAdapterInstanceNumber + '.current.temperature', true, {
type: 'number',
@@ -2603,6 +2621,57 @@ async function CreateWeatherAlias () {
} catch (err: any) {
log('error at function CreateWeatherAlias brightsky.' + weatherAdapterInstanceNumber + '.: ' + err.message, 'warn');
}
} else if (weatherAdapterInstance == 'swiss-weather-api.' + weatherAdapterInstanceNumber + '.') {
try {
if (isSetOptionActive) {
if (!existsState(config.weatherEntity + '.ICON') && existsState('swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day0.0000.symbol_code')) {
log('Weather alias for swiss-weather-api.' + weatherAdapterInstanceNumber + '. does not exist yet, will be created now', 'info');
setObject(config.weatherEntity, {
_id: config.weatherEntity,
type: 'channel',
common: {role: 'weatherCurrent', name: 'weatherCurrent'},
native: {}
});
await createAliasAsync(config.weatherEntity + '.ICON', ('swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day0.0000.symbol_code'), true, {
type: 'string',
role: 'value',
name: 'ICON',
alias: {id: 'swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day0.0000.symbol_code'},
});
await createAliasAsync(config.weatherEntity + '.TEMP', 'swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.current_hour.TTT_C', true, {
type: 'number',
role: 'value.temperature',
name: 'TEMP',
alias: {
id: 'swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.current_hour.TTT_C',
read: 'Math.round(val*10)/10'
},
});
await createAliasAsync(config.weatherEntity + '.TEMP_MIN', 'swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day0.0000.TN_C', true, {
type: 'number',
role: 'value.temperature.forecast.0',
name: 'TEMP_MIN',
alias: {
id: 'swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day0.0000.TN_C',
read: 'Math.round(val)'
},
});
await createAliasAsync(config.weatherEntity + '.TEMP_MAX', 'swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day0.0000.TX_C', true, {
type: 'number',
role: 'value.temperature.max.forecast.0',
name: 'TEMP_MAX',
alias: {
id: 'swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day0.0000.TX_C',
read: 'Math.round(val)'
},
});
} else {
log('weather alias for swiss-weather-api.' + weatherAdapterInstanceNumber + '. already exists', 'info');
}
}
} catch (err: any) {
log('error at function CreateWeatherAlias swiss-weather-api.' + weatherAdapterInstanceNumber + '.: ' + err.message, 'warn');
}
}
}
} catch (err: any) {
@@ -3218,6 +3287,8 @@ async function InitPopupNotify () {
'~' +
v_popupNotifyButton1TextColor +
'~' +
'~' + // Fix Button3_Text for Adapter
'~' + // Fix Button3_Color for Adapter
getState(popupNotifyButton2Text).val +
'~' +
v_popupNotifyButton2TextColor +
@@ -4664,6 +4735,7 @@ function HandleMessage (typ: string, method: NSPanel.EventMethod, page: number |
}
}
break;
case 'buttonPress3':
case 'buttonPress2':
screensaverEnabled = false;
HandleButtonEvent(words);
@@ -5170,7 +5242,7 @@ function CreateEntity (pageItem: PageItem, placeId: number, useColors: boolean =
val = getState(pageItem.id + '.ACTUAL').val;
RegisterEntityWatcher(pageItem.id + '.ACTUAL');
}
if (existsState(pageItem.id + '.SET')) {
if (existsState(pageItem.id + '.SET') && !existsState(pageItem.id + 'ACTUAL')) {
val = getState(pageItem.id + '.SET').val;
RegisterEntityWatcher(pageItem.id + '.SET');
}
@@ -5183,7 +5255,7 @@ function CreateEntity (pageItem: PageItem, placeId: number, useColors: boolean =
val = getState(pageItem.id + '.ON_SET').val;
RegisterEntityWatcher(pageItem.id + '.ON_SET');
}
if (existsState(pageItem.id + '.ON')) {
if (existsState(pageItem.id + '.ON') && !existsState(pageItem.id + '.ON_ACTUAL')) {
val = getState(pageItem.id + '.ON').val;
RegisterEntityWatcher(pageItem.id + '.ON');
}
@@ -6350,7 +6422,7 @@ function RegisterEntityWatcher (id: string): void {
return;
}
subscriptions[id] = on({id: id, change: 'any'}, (obj) => {
subscriptions[id] = on({id: id, change: 'ne'}, (obj) => {
//@ts-ignore
if (obj.oldState && obj.oldState.val === obj.state.val && obj.state.ack) {
return;
@@ -8597,7 +8669,7 @@ async function createAutoQRAlias (id: string, dpPath: string) {
if (autoCreateAlias) {
if (isSetOptionActive) {
if (existsState(dpPath + 'Daten') == false) {
await createStateAsync(dpPath + 'Daten', 'WIFI:T:undefined;S:undefined;P:undefined;H:undefined;', {type: 'string', write: true});
await createStateAsync(dpPath + 'Daten', 'WIFI:T:undefined;S:undefined;P:undefined;H:;', {type: 'string', write: true});
await createStateAsync(dpPath + 'Switch', false, {type: 'boolean', write: true});
setObject(id, {_id: id, type: 'channel', common: {role: 'switch.mode.wlan', name: 'QR Page'}, native: {}});
await createAliasAsync(id + '.ACTUAL', dpPath + 'Daten', true, {type: 'string', role: 'state', name: 'ACTUAL'});
@@ -8642,7 +8714,7 @@ function GenerateQRPage (page: NSPanel.PageQR): NSPanel.Payload[] {
let o = getObject(id);
let heading = page.heading !== undefined ? page.heading : typeof o.common.name === 'object' ? o.common.name.de : o.common.name;
let textQR = page.items[0].id + '.ACTUAL' !== undefined ? getState(page.items[0].id + '.ACTUAL').val : 'WIFI:T:undefined;S:undefined;P:undefined;H:undefined;';
let textQR = page.items[0].id + '.ACTUAL' !== undefined ? getState(page.items[0].id + '.ACTUAL').val : 'WIFI:T:undefined;S:undefined;P:undefined;H:;';
let hiddenPWD = false;
if (page.items[0].hidePassword !== undefined && page.items[0].hidePassword == true) {
hiddenPWD = true;
@@ -8780,7 +8852,11 @@ function unsubscribePowerSubscriptions (): void {
* @returns {void}
*/
function subscribePowerSubscriptions (id: string): void {
on({id: id + '.ACTUAL', change: 'ne'}, async function () {
const subscriptionKey = 'power_' + id + '.ACTUAL';
if (subscriptions.hasOwnProperty(subscriptionKey)) {
return;
}
subscriptions[subscriptionKey] = on({id: id + '.ACTUAL', change: 'ne'}, async function () {
(function () {
if (timeoutPower) {
clearTimeout(timeoutPower);
@@ -8793,7 +8869,6 @@ function subscribePowerSubscriptions (id: string): void {
});
}
/**
* @function GeneratePowerPage
* @description Generates a page with power state and energy usage information.
@@ -9331,10 +9406,10 @@ function HandleButtonEvent (words: any): void {
}
break;
case 'notifyAction':
if (words[4] == 'yes') {
if (words[4] == 'button1') { //Changes button1 retuns "button1" instead of "yes"
setState(popupNotifyInternalName, {val: words[2], ack: true});
setState(popupNotifyAction, {val: true, ack: true});
} else if (words[4] == 'no') {
} else if (words[4] == 'button3') { //Changes button3 retuns "button3" instead of "no" --> button2 has no functionality in Script (only Adapter)
setState(popupNotifyInternalName, {val: words[2], ack: true});
setState(popupNotifyAction, {val: false, ack: true});
}
@@ -10119,6 +10194,10 @@ function HandleButtonEvent (words: any): void {
break;
}
}
} else {
let pageItemSlider = findPageItem(id);
let sliderPos = Math.trunc(scale(parseInt(words[4]), 0, 100, pageItemSlider.maxValueLevel ? pageItemSlider.maxValue : 100, pageItemSlider.minValueLevel ? pageItemSlider.minValue : 0));
setIfExists(pageItemSlider.id + '.SET', sliderPos) ? true : setIfExists(id + '.ACTUAL', sliderPos);
}
break;
case 'mode-seek':
@@ -10434,7 +10513,11 @@ function HandleButtonEvent (words: any): void {
break;
}
} catch (err: any) {
log('error at function HandleButtonEvent: ' + err.message, 'warn');
if (err.message == "Cannot read properties of undefined (reading 'id')") {
} else {
log('error at function HandleButtonEvent: ' + err.message, 'warn');
}
}
}
@@ -11375,7 +11458,7 @@ function GenerateDetailPage (type: NSPanel.PopupType, optional: NSPanel.mediaOpt
let hSlider2MinVal: number = pageItem.minValue ?? 0;
let hSlider2MaxVal: number = pageItem.maxValue ?? 100;
let hSlider2ZeroVal: number = 0;
let hSlider2CurVal: number = getState(id + '.ACTUAL').val;
let hSlider2CurVal: number = existsState(id + '.ACTUAL') ? getState(id + '.ACTUAL').val : getState(id + '.SET').val;
let hSlider2Step: number = 1;
let hSlider2Visibility: string = "enable";
@@ -12391,85 +12474,100 @@ function HandleScreensaverUpdate (): void {
} else if (weatherAdapterInstance == 'openweathermap.' + weatherAdapterInstanceNumber + '.') {
entityIcon = Icons.GetIcon(GetOpenWeatherMapIcon(icon));
entityIconCol = GetOpenWeatherMapIconColor(icon);
} else if (weatherAdapterInstance == 'swiss-weather-api.' + weatherAdapterInstanceNumber + '.') {
entityIcon = Icons.GetIcon(GetSwissWeatherApiIcon(icon));
entityIconCol = GetSwissWeatherApiIconColor(icon);
} else if (weatherAdapterInstance == 'pirate-weather.' + weatherAdapterInstanceNumber + '.' || weatherAdapterInstance == 'brightsky.' + weatherAdapterInstanceNumber + '.') {
entityIcon = Icons.GetIcon(GetPirateWeatherIcon(icon));
entityIconCol = GetPirateWeatherIconColor(icon);
} else if (weatherAdapterInstance == 'brightsky.' + weatherAdapterInstanceNumber + '.') {
entityIcon = Icons.GetIcon(icon);
entityIconCol = GetBrightskyWeatherIconColor(icon);
}
payloadString += '~' + '~' + entityIcon + '~' + entityIconCol + '~' + '~' + optionalValue + '~';
}
// 3 leftScreensaverEntities
if (screensaverAdvanced) {
let checkpoint = true;
let i = 0;
if (config.leftScreensaverEntity && Array.isArray(config.leftScreensaverEntity) && config.leftScreensaverEntity.length > 0) {
for (i = 0; i < 3 && i < config.leftScreensaverEntity.length; i++) {
const leftScreensaverEntity = config.leftScreensaverEntity[i];
if (leftScreensaverEntity === null || leftScreensaverEntity === undefined) {
checkpoint = false;
break;
}
RegisterScreensaverEntityWatcher(leftScreensaverEntity.ScreensaverEntity);
let val = getState(leftScreensaverEntity.ScreensaverEntity).val;
let iconColor = rgb_dec565(White);
let icon;
if (typeof leftScreensaverEntity.ScreensaverEntityIconOn == 'string' && existsObject(leftScreensaverEntity.ScreensaverEntityIconOn as string)) {
let iconName = getState(leftScreensaverEntity.ScreensaverEntityIconOn!).val;
icon = Icons.GetIcon(iconName);
} else {
icon = Icons.GetIcon(leftScreensaverEntity.ScreensaverEntityIconOn);
}
if (parseFloat(val + '') == val) {
val = parseFloat(val);
}
if (typeof val == 'number') {
val = val * (leftScreensaverEntity.ScreensaverEntityFactor ? leftScreensaverEntity.ScreensaverEntityFactor! : 0)
icon = determineScreensaverStatusIcon(leftScreensaverEntity,val,icon)
val = val.toFixed(
leftScreensaverEntity.ScreensaverEntityDecimalPlaces
) + leftScreensaverEntity.ScreensaverEntityUnitText;
iconColor = GetScreenSaverEntityColor(leftScreensaverEntity);
} else if (typeof val == 'boolean') {
iconColor = GetScreenSaverEntityColor(leftScreensaverEntity);
if (!val && leftScreensaverEntity.ScreensaverEntityIconOff != null) {
icon = Icons.GetIcon(leftScreensaverEntity.ScreensaverEntityIconOff);
}
} else if (typeof val == 'string') {
iconColor = GetScreenSaverEntityColor(leftScreensaverEntity);
let pformat = parseFormat(val);
if (Debug) log('moments.js --> Datum ' + val + ' valid?: ' + moment(val, pformat, true).isValid(), 'info');
if (moment(val, pformat, true).isValid()) {
let DatumZeit = moment(val, pformat).unix(); // Umwandlung in Unix Time-Stamp
if (leftScreensaverEntity.ScreensaverEntityDateFormat !== undefined) {
val = new Date(DatumZeit * 1000).toLocaleString(getState(NSPanel_Path + 'Config.locale').val, leftScreensaverEntity.ScreensaverEntityDateFormat);
} else {
val = new Date(DatumZeit * 1000).toLocaleString(getState(NSPanel_Path + 'Config.locale').val);
}
}
}
const temp = leftScreensaverEntity.ScreensaverEntityIconColor;
if (temp && typeof temp == 'string' && existsObject(temp)) {
iconColor = getState(temp).val;
}
payloadString += '~' + '~' + icon + '~' + iconColor + '~' + leftScreensaverEntity.ScreensaverEntityText + '~' + val + '~';
}
}
if (i < 3) {
checkpoint = false;
}
if (checkpoint == false) {
for (let j = i; j < 3; j++) {
payloadString += '~~~~~~';
}
}
}
if (screensaverAdvanced) {
let checkpoint = true;
let i = 0;
if (config.leftScreensaverEntity && Array.isArray(config.leftScreensaverEntity) && config.leftScreensaverEntity.length > 0) {
for (i = 0; i < 3 && i < config.leftScreensaverEntity.length; i++) {
const leftScreensaverEntity = config.leftScreensaverEntity[i];
if (leftScreensaverEntity === null || leftScreensaverEntity === undefined) {
checkpoint = false;
break;
}
RegisterScreensaverEntityWatcher(leftScreensaverEntity.ScreensaverEntity);
let val = getState(leftScreensaverEntity.ScreensaverEntity).val;
let iconColor = rgb_dec565(White);
let icon;
if (typeof leftScreensaverEntity.ScreensaverEntityIconOn == 'string' && existsObject(leftScreensaverEntity.ScreensaverEntityIconOn as string)) {
let iconName = getState(leftScreensaverEntity.ScreensaverEntityIconOn!).val;
icon = Icons.GetIcon(iconName);
} else {
icon = Icons.GetIcon(leftScreensaverEntity.ScreensaverEntityIconOn);
}
if (parseFloat(val + '') == val) {
val = parseFloat(val);
}
if (typeof val == 'number') {
val = val * (leftScreensaverEntity.ScreensaverEntityFactor ? leftScreensaverEntity.ScreensaverEntityFactor! : 0)
icon = determineScreensaverStatusIcon(leftScreensaverEntity, val, icon)
// Einheit ermitteln: String oder aus DP
let unitText = '';
if (typeof leftScreensaverEntity.ScreensaverEntityUnitText === 'string') {
if (existsObject(leftScreensaverEntity.ScreensaverEntityUnitText)) {
unitText = getState(leftScreensaverEntity.ScreensaverEntityUnitText).val;
} else {
unitText = leftScreensaverEntity.ScreensaverEntityUnitText;
}
}
val = val.toFixed(leftScreensaverEntity.ScreensaverEntityDecimalPlaces) + unitText;
iconColor = GetScreenSaverEntityColor(leftScreensaverEntity);
} else if (typeof val == 'boolean') {
iconColor = GetScreenSaverEntityColor(leftScreensaverEntity);
if (!val && leftScreensaverEntity.ScreensaverEntityIconOff != null) {
icon = Icons.GetIcon(leftScreensaverEntity.ScreensaverEntityIconOff);
}
} else if (typeof val == 'string') {
iconColor = GetScreenSaverEntityColor(leftScreensaverEntity);
let pformat = parseFormat(val);
if (Debug) log('moments.js --> Datum ' + val + ' valid?: ' + moment(val, pformat, true).isValid(), 'info');
if (moment(val, pformat, true).isValid()) {
let DatumZeit = moment(val, pformat).unix(); // Umwandlung in Unix Time-Stamp
if (leftScreensaverEntity.ScreensaverEntityDateFormat !== undefined) {
val = new Date(DatumZeit * 1000).toLocaleString(getState(NSPanel_Path + 'Config.locale').val, leftScreensaverEntity.ScreensaverEntityDateFormat);
} else {
val = new Date(DatumZeit * 1000).toLocaleString(getState(NSPanel_Path + 'Config.locale').val);
}
}
}
const temp = leftScreensaverEntity.ScreensaverEntityIconColor;
if (temp && typeof temp == 'string' && existsObject(temp)) {
iconColor = getState(temp).val;
}
payloadString += '~' + '~' + icon + '~' + iconColor + '~' + leftScreensaverEntity.ScreensaverEntityText + '~' + val + '~';
}
}
if (i < 3) {
checkpoint = false;
}
if (checkpoint == false) {
for (let j = i; j < 3; j++) {
payloadString += '~~~~~~';
}
}
}
// 6 bottomScreensaverEntities
let maxEntities: number = 7;
@@ -12594,17 +12692,41 @@ function HandleScreensaverUpdate (): void {
DayOfWeek = existsObject('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.timestamp')
? formatDate(getDateObject((getState('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.timestamp').val)), 'W', 'de')
: 0;
WeatherIcon = existsObject('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.icon')
? GetPirateWeatherIcon(getState('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.icon').val)
WeatherIcon = existsObject('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.icon_special')
? getState('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.icon_special').val
: '';
WheatherColor = existsObject('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.icon')
? GetPirateWeatherIconColor(String(getState('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.icon').val))
WheatherColor = existsObject('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.icon_special')
? GetBrightskyWeatherIconColor(String(getState('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.icon_special').val))
: 0;
RegisterScreensaverEntityWatcher('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.temperature_min');
RegisterScreensaverEntityWatcher('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.temperature_max');
RegisterScreensaverEntityWatcher('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.timestamp');
RegisterScreensaverEntityWatcher('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.icon');
RegisterScreensaverEntityWatcher('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.icon_special');
}
} else if (weatherAdapterInstance == 'swiss-weather-api.' + weatherAdapterInstanceNumber + '.') {
if (i < 6) {
// swiss-weather-api. 0 .forecast.days.day 0 .0000.TN_C
TempMin = existsObject('swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day' + String(i - 1) + '.0000.TN_C')
? Math.round(getState('swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day' + String(i - 1) + '.0000.TN_C').val * 10) / 10
: 0;
TempMax = existsObject('swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day' + String(i - 1) + '.0000.TX_C')
? Math.round(getState('swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day' + String(i - 1) + '.0000.TX_C').val * 10) / 10
: 0;
DayOfWeek = existsObject('swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day' + String(i - 1) + '.0000.day_name')
? getState('swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day' + String(i - 1) + '.0000.day_name').val
: 0;
WeatherIcon = existsObject('swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day' + String(i - 1) + '.0000.symbol_code')
? GetSwissWeatherApiIcon(String(getState('swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day' + String(i - 1) + '.0000.symbol_code').val))
: '';
WheatherColor = existsObject('swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day' + String(i - 1) + '.0000.symbol_code')
? GetSwissWeatherApiIconColor(String(getState('swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day' + String(i - 1) + '.0000.symbol_code').val))
: 0;
RegisterScreensaverEntityWatcher('swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day' + String(i - 1) + '.0000.TN_C');
RegisterScreensaverEntityWatcher('swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day' + String(i - 1) + '.0000.TX_C');
RegisterScreensaverEntityWatcher('swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day' + String(i - 1) + '.0000.day_name');
RegisterScreensaverEntityWatcher('swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day' + String(i - 1) + '.0000.symbol_code');
}
}
@@ -12714,6 +12836,30 @@ function HandleScreensaverUpdate (): void {
sun = 'weather-sunset-up';
}
payloadString += '~' + '~' + Icons.GetIcon(sun) + '~' + rgb_dec565(MSYellow) + '~' + 'Sonne' + '~' + formatDate(getDateObject(arraySunEvent[nextSunEvent]), 'hh:mm') + '~';
} else if (weatherAdapterInstance == 'swiss-weather-api.' + weatherAdapterInstanceNumber + '.' && i == 6) {
let nextSunEvent = 0;
let valDateNow = getDateObject((new Date().getTime())).getTime();
let arraySunEvent: number[] = [];
arraySunEvent[0] = getDateObject(getState('swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day0.0000.SUNRISE').val).getTime();
arraySunEvent[1] = getDateObject(getState('swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day0.0000.SUNSET').val).getTime();
arraySunEvent[2] = getDateObject(getState('swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day1.0000.SUNRISE').val).getTime();
let j = 0;
for (j = 0; j < 3; j++) {
if (arraySunEvent[j] > valDateNow) {
nextSunEvent = j;
break;
}
}
let sun = '';
if (j == 1) {
sun = 'weather-sunset-down';
} else {
sun = 'weather-sunset-up';
}
payloadString += '~' + '~' + Icons.GetIcon(sun) + '~' + rgb_dec565(MSYellow) + '~' + 'Sonne' + '~' + formatDate(getDateObject(arraySunEvent[nextSunEvent]), 'hh:mm') + '~';
} else {
payloadString += '~' + '~' + Icons.GetIcon(WeatherIcon) + '~' + WheatherColor + '~' + DayOfWeek + '~' + tempMinMaxString + '~';
@@ -13686,6 +13832,193 @@ function GetOpenWeatherMapIconColor (icon: string): number {
return 0;
}
/**
* Retrieves the SwissWeatherApi icon string based on the provided icon string.
*
* This function maps the given SwissWeatherApi icon string to its corresponding icon string representation.
* See https://github.com/baerengraben/ioBroker.swiss-weather-api/tree/master/img/Meteo_API_Icons/Color for
* list of icons.
*
* @function GetSwissWeatherApiIcon
* @param {string} icon - The icon string.
* @returns {string} The corresponding icon string.
*/
function GetSwissWeatherApiIcon(icon: string): string {
try {
switch (icon) {
case "1":
return 'weather-sunny';
case "-1":
return 'weather-night';
case "3": //few clouds day
return 'weather-partly-cloudy';
case "-3": //few clouds night
return 'weather-night-partly-cloudy';
case "10": //scattered clouds
case "-10":
return 'weather-cloudy';
case "18": //cloudy
case "-18":
case "19":
case "-19":
return 'weather-cloudy';
case "23": //shower rain
case "-23":
return 'weather-rainy';
case "4": //rain
case "-4":
case "8":
case "-8":
case "11":
case "-11":
case "15":
case "-15":
case "20":
case "-20":
case "22":
case "-22":
case "25":
case "-25":
case "29":
case "-29":
return 'weather-pouring';
case "5": //Thunderstorm
case "-5":
case "7":
case "-7":
case "9":
case "-9":
case "12":
case "-12":
case "14":
case "-14":
case "16":
case "-16":
case "26":
case "-26":
case "28":
case "-28":
case "30":
case "-30":
return 'weather-lightning';
case "6": //snow
case "-6":
case "13":
case "-13":
case "21":
case "-21":
case "24":
case "-24":
case "27":
case "-27":
return 'weather-snowy';
case "2": //mist
case "-2":
case "17":
case "-17":
return 'weather-fog';
default:
return 'alert-circle-outline';
}
} catch (err: any) {
log('error at function GetSwissWeatherApiIcon: ' + err.message, 'warn');
}
return '';
}
/**
* Retrieves the color code for a given SwissWeatherApi icon string.
*
* This function maps the provided SwissWeatherApi icon string to its corresponding color code.
* See https://github.com/baerengraben/ioBroker.swiss-weather-api/tree/master/img/Meteo_API_Icons/Color for
* list of icons.
*
* @function GetSwissWeatherApiIconColor
* @param {string} icon - The icon string.
* @returns {number} The corresponding color code.
*/
function GetSwissWeatherApiIconColor(icon: string): number {
try {
switch (icon) {
case "1": //clear sky day
return rgb_dec565(swSunny);
case "-1": //clear sky night
return rgb_dec565(swClearNight);
case "3": //few clouds day
case "-3": //few clouds night
return rgb_dec565(swPartlycloudy);
case "10": //scattered clouds
case "-10":
return rgb_dec565(swCloudy);
case "18": //cloudy
case "-18":
case "19":
case "-19":
return rgb_dec565(swCloudy);
case "23": //shower rain
case "-23":
return rgb_dec565(swRainy);
case "4": //rain
case "-4":
case "8":
case "-8":
case "11":
case "-11":
case "15":
case "-15":
case "20":
case "-20":
case "22":
case "-22":
case "25":
case "-25":
case "29":
case "-29":
return rgb_dec565(swPouring);
case "5": //Thunderstorm
case "-5":
case "7":
case "-7":
case "9":
case "-9":
case "12":
case "-12":
case "14":
case "-14":
case "16":
case "-16":
case "26":
case "-26":
case "28":
case "-28":
case "30":
case "-30":
return rgb_dec565(swLightningRainy);
case "6": //snow
case "-6":
case "13":
case "-13":
case "21":
case "-21":
case "24":
case "-24":
case "27":
case "-27":
return rgb_dec565(swSnowy);
case "2": //mist
case "-2":
case "17":
case "-17":
return rgb_dec565(swFog);
default:
return rgb_dec565(White);
}
} catch (err: any) {
log('error at function GetSwissWeatherApiIconColor: ' + err.message, 'warn');
}
return 0;
}
/**
* Retrieves the PirateWeather icon string based on the provided icon string.
*
@@ -13875,6 +14208,82 @@ function GetPirateWeatherIconColor (icon: string): number {
return 0;
}
function GetBrightskyWeatherIconColor (icon: string): number {
try {
switch (icon) {
case 'weather-cloudy':
return rgb_dec565(swCloudy); // cloudy
case 'weather-fog':
return rgb_dec565(swFog);
case 'weather-hail':
return rgb_dec565(swHail);
case 'weather-hazy':
return rgb_dec565(swFog);
case 'weather-lightning':
return rgb_dec565(swLightning);
case 'weather-lightning-rainy':
return rgb_dec565(swLightningRainy);
case 'weather-night':
return rgb_dec565(swClearNight);
case 'weather-night-partly-cloudy':
return rgb_dec565(swPartlycloudy);
case 'weather-partly-cloudy':
return rgb_dec565(swPartlycloudy);
case 'weather-partly-rainy':
return rgb_dec565(swRainy);
case 'weather-partly-snowy':
return rgb_dec565(swSnowy);
case 'weather-partly-snowy-rainy':
return rgb_dec565(swSnowyRainy);
case 'weather-pouring':
return rgb_dec565(swPouring);
case 'weather-rainy':
return rgb_dec565(swRainy);
case 'weather-snowy':
return rgb_dec565(swSnowy);
case 'weather-snowy-heavy':
return rgb_dec565(swSnowy);
case 'weather-snowy-rainy':
return rgb_dec565(swSnowyRainy);
case 'weather-sunny':
return rgb_dec565(swSunny);
case 'weather-tornado':
return rgb_dec565(swExceptional);
case 'weather-windy':
return rgb_dec565(swWindy);
case 'weather-windy-variant':
return rgb_dec565(swWindy);
default:
return rgb_dec565(swExceptional);
}
} catch (err: any) {
log('error at function GetPirateWeatherIcon: ' + err.message, 'warn');
}
return rgb_dec565(swExceptional);
}
//------------------Begin Read Internal Sensor Data
//mqttCallback (topic: string, message: string): Promise<void> {
/**
@@ -14564,6 +14973,7 @@ function isEventMethod (F: string | NSPanel.EventMethod): F is NSPanel.EventMeth
case 'sleepReached':
case 'pageOpenDetail':
case 'buttonPress2':
case 'buttonPress3':
case 'renderCurrentPage':
case 'button1':
case 'button2':
@@ -14628,7 +15038,7 @@ function isPagePower (F: NSPanel.PageType | NSPanel.PagePower): F is NSPanel.Pag
namespace NSPanel {
export type PopupType = 'popupFan' | 'popupInSel' | 'popupLight' | 'popupNotify' | 'popupShutter' | 'popupShutter2' | 'popupSlider' | 'popupThermo' | 'popupTimer' | 'popupColor';
export type EventMethod = 'startup' | 'sleepReached' | 'pageOpenDetail' | 'buttonPress2' | 'renderCurrentPage' | 'button1' | 'button2';
export type EventMethod = 'startup' | 'sleepReached' | 'pageOpenDetail' | 'buttonPress2' | 'buttonPress3' | 'renderCurrentPage' | 'button1' | 'button2';
export type panelRecvType = {
event: 'event';
method: EventMethod;

View File

@@ -12,7 +12,7 @@ repo_name: jobr99/nspanel-lovelace-ui
repo_url: https://github.com/jobr99/nspanel-lovelace-ui
edit_uri: ""
copyright: "Copyright &copy; 2023 Johannes Braun"
copyright: "Copyright &copy; 2026 Johannes Braun"
docs_dir: docs
@@ -42,8 +42,6 @@ extra_css:
extra:
analytics:
provider: custom
version:
provider: mike
markdown_extensions:
- admonition
@@ -63,7 +61,6 @@ markdown_extensions:
plugins:
- search:
lang: en
- mkdocs-video
nav:
- "Overview": index.md
@@ -75,6 +72,7 @@ nav:
- "FAQ": faq.md
- "Configuration - apps.yaml (Home Assistant)":
- "Overview": config-overview.md
#- "Migration to Standalone Rewrite": config-migration-standalone.md
- "Screensaver": config-screensaver.md
- "Cards":
- "Entities Card": card-entities.md

View File

@@ -1,6 +1,6 @@
# https://developers.home-assistant.io/docs/add-ons/configuration#add-on-config
name: NSPanel Lovelace UI Addon
version: "4.7.80"
version: "4.7.85"
slug: nspanel-lovelace-ui
description: NSPanel Lovelace UI Addon
services:

View File

@@ -475,7 +475,7 @@ class AlarmCard(HACard):
main_entity = self.entities[0]
main_entity.render()
print(main_entity.state)
logging.debug("Alarm card state for '%s': %s", main_entity.entity_id, main_entity.state)
icon = get_icon_char("shield-off")
color = rgb_dec565([255,255,255])

View File

@@ -8,6 +8,8 @@ def wait_for_ha_cache():
while time.time() < mustend:
if len(libs.home_assistant.home_assistant_entity_state_cache) == 0:
time.sleep(0.1)
if len(libs.home_assistant.home_assistant_entity_state_cache) == 0:
logging.warning("Home Assistant entity cache is still empty after waiting 5 seconds")
time.sleep(1)
def calculate_dim_values(sleepTracking, sleepTrackingZones, sleepBrightness, screenBrightness, sleepOverride, return_involved_entities=False):
@@ -28,8 +30,8 @@ def calculate_dim_values(sleepTracking, sleepTrackingZones, sleepBrightness, scr
involved_entities.append(sleepBrightness)
try:
dimmode = int(float(libs.home_assistant.get_entity_data(sleepBrightness).get('state', 10)))
except ValueError:
print("sleepBrightness entity invalid")
except (TypeError, ValueError):
logging.exception("sleepBrightness entity '%s' has an invalid state value", sleepBrightness)
if screenBrightness:
if isinstance(screenBrightness, int):
@@ -44,8 +46,8 @@ def calculate_dim_values(sleepTracking, sleepTrackingZones, sleepBrightness, scr
involved_entities.append(screenBrightness)
try:
dimValueNormal = int(float(libs.home_assistant.get_entity_data(screenBrightness).get('state', 100)))
except ValueError:
print("screenBrightness entity invalid")
except (TypeError, ValueError):
logging.exception("screenBrightness entity '%s' has an invalid state value", screenBrightness)
# force sleep brightness to zero in case sleepTracking is active
if sleepTracking:
if libs.home_assistant.is_existent(sleepTracking):
@@ -69,12 +71,13 @@ def calculate_dim_values(sleepTracking, sleepTrackingZones, sleepBrightness, scr
else:
return dimmode, dimValueNormal
def handle_buttons(entity_id, btype, value, entity_config=None):
def handle_buttons(entity_id, btype, value, entity_config=None, action_context=None):
action_context = action_context or {}
match btype:
case 'button':
button_press(entity_id, value)
button_press(entity_id, value, action_context=action_context)
case 'OnOff':
on_off(entity_id, value)
on_off(entity_id, value, action_context=action_context)
case 'number-set':
if entity_id.startswith('fan'):
attr = libs.home_assistant.get_entity_data(entity_id).get('attributes', [])
@@ -82,7 +85,7 @@ def handle_buttons(entity_id, btype, value, entity_config=None):
service_data = {
"value": int(value)
}
call_ha_service(entity_id, "set_value", service_data=service_data)
call_ha_service(entity_id, "set_value", service_data=service_data, action_context=action_context)
case 'up' | 'stop' | 'down' | 'tiltOpen' | 'tiltStop' | 'tiltClose' | 'media-next' | 'media-back' | 'media-pause' | 'timer-cancel' | 'timer-pause' | 'timer-finish':
action_service_mapping = {
'up': 'open_cover',
@@ -99,37 +102,37 @@ def handle_buttons(entity_id, btype, value, entity_config=None):
'timer-finish': 'finish',
}
service = action_service_mapping[btype]
call_ha_service(entity_id, service)
call_ha_service(entity_id, service, action_context=action_context)
case 'timer-start':
if value:
service_data = {
"duration": value
}
call_ha_service(entity_id, "start", service_data=service_data)
call_ha_service(entity_id, "start", service_data=service_data, action_context=action_context)
else:
call_ha_service(entity_id, "start")
call_ha_service(entity_id, "start", action_context=action_context)
case 'positionSlider':
service_data = {
"position": int(value)
}
call_ha_service(entity_id, "set_cover_position", service_data=service_data)
call_ha_service(entity_id, "set_cover_position", service_data=service_data, action_context=action_context)
case 'tiltSlider':
service_data = {
"tilt_position": int(value)
}
call_ha_service(entity_id, "set_cover_tilt_position", service_data=service_data)
call_ha_service(entity_id, "set_cover_tilt_position", service_data=service_data, action_context=action_context)
case 'media-OnOff':
state = libs.home_assistant.get_entity_data(entity_id).get('state', '')
if state == "off":
call_ha_service(entity_id, "turn_on")
call_ha_service(entity_id, "turn_on", action_context=action_context)
else:
call_ha_service(entity_id, "turn_off")
call_ha_service(entity_id, "turn_off", action_context=action_context)
case 'media-shuffle':
suffle = libs.home_assistant.get_entity_data(entity_id).get('attributes', []).get('shuffle')
service_data = {
"shuffle": not suffle
}
call_ha_service(entity_id, "set_value", service_data=service_data)
call_ha_service(entity_id, "set_value", service_data=service_data, action_context=action_context)
case 'volumeSlider':
pos = int(value)
# HA wants to have this value between 0 and 1 as float
@@ -137,12 +140,12 @@ def handle_buttons(entity_id, btype, value, entity_config=None):
service_data = {
"volume_level": pos
}
call_ha_service(entity_id, "volume_set", service_data=service_data)
call_ha_service(entity_id, "volume_set", service_data=service_data, action_context=action_context)
case 'speaker-sel':
service_data = {
"volume_level": value
}
call_ha_service(entity_id, "select_source", service_data=service_data)
call_ha_service(entity_id, "select_source", service_data=service_data, action_context=action_context)
# for light detail page
case 'brightnessSlider':
# scale 0-100 to ha brightness range
@@ -150,7 +153,7 @@ def handle_buttons(entity_id, btype, value, entity_config=None):
service_data = {
"brightness": brightness
}
call_ha_service(entity_id, "turn_on", service_data=service_data)
call_ha_service(entity_id, "turn_on", service_data=service_data, action_context=action_context)
case 'colorTempSlider':
attr = libs.home_assistant.get_entity_data(entity_id).get('attributes', [])
min_mireds = attr.get("min_mireds")
@@ -160,19 +163,19 @@ def handle_buttons(entity_id, btype, value, entity_config=None):
service_data = {
"color_temp": color_val
}
call_ha_service(entity_id, "turn_on", service_data=service_data)
call_ha_service(entity_id, "turn_on", service_data=service_data, action_context=action_context)
case 'colorWheel':
value = value.split('|')
color = pos_to_color(int(value[0]), int(value[1]), int(value[2]))
service_data = {
"rgb_color": color
}
call_ha_service(entity_id, "turn_on", service_data=service_data)
call_ha_service(entity_id, "turn_on", service_data=service_data, action_context=action_context)
case 'disarm' | 'arm_home' | 'arm_away' | 'arm_night' | 'arm_vacation':
service_data = {
"code": value
}
call_ha_service(entity_id, f"alarm_{btype}", service_data=service_data)
call_ha_service(entity_id, f"alarm_{btype}", service_data=service_data, action_context=action_context)
case 'mode-preset_modes' | 'mode-swing_modes' | 'mode-fan_modes':
attr = libs.home_assistant.get_entity_data(entity_id).get('attributes', [])
mapping = {
@@ -187,7 +190,7 @@ def handle_buttons(entity_id, btype, value, entity_config=None):
service_data = {
mapping[btype][:-1]: mode
}
call_ha_service(entity_id, f"set_{mapping[btype][:-1]}", service_data=service_data)
call_ha_service(entity_id, f"set_{mapping[btype][:-1]}", service_data=service_data, action_context=action_context)
case 'mode-input_select' | 'mode-select':
options = libs.home_assistant.get_entity_data(entity_id).get('attributes', []).get("options", [])
if options:
@@ -195,7 +198,7 @@ def handle_buttons(entity_id, btype, value, entity_config=None):
service_data = {
"option": option
}
call_ha_service(entity_id, "select_option", service_data=service_data)
call_ha_service(entity_id, "select_option", service_data=service_data, action_context=action_context)
case 'mode-media_player':
options = libs.home_assistant.get_entity_data(entity_id).get('attributes', []).get("source_list", [])
if options:
@@ -203,7 +206,7 @@ def handle_buttons(entity_id, btype, value, entity_config=None):
service_data = {
"source": option
}
call_ha_service(entity_id, "select_source", service_data=service_data)
call_ha_service(entity_id, "select_source", service_data=service_data, action_context=action_context)
case 'mode-light':
options = entity_config.get("effectList", libs.home_assistant.get_entity_data(entity_id).get('attributes', []).get("effect_list", []))
if options:
@@ -211,13 +214,13 @@ def handle_buttons(entity_id, btype, value, entity_config=None):
service_data = {
"effect": option
}
call_ha_service(entity_id, "turn_on", service_data=service_data)
call_ha_service(entity_id, "turn_on", service_data=service_data, action_context=action_context)
case 'tempUpd':
temp = int(value)/10
service_data = {
"temperature": temp
}
call_ha_service(entity_id, "set_temperature", service_data=service_data)
call_ha_service(entity_id, "set_temperature", service_data=service_data, action_context=action_context)
case 'tempUpdHighLow':
value = value.split("|")
temp_high = int(value[0])/10
@@ -226,47 +229,67 @@ def handle_buttons(entity_id, btype, value, entity_config=None):
"target_temp_high": temp_high,
"target_temp_low": temp_low,
}
call_ha_service(entity_id, "set_temperature", service_data=service_data)
call_ha_service(entity_id, "set_temperature", service_data=service_data, action_context=action_context)
case 'hvac_action':
service_data = {
"hvac_mode": value
}
call_ha_service(entity_id, "set_hvac_mode", service_data=service_data)
call_ha_service(entity_id, "set_hvac_mode", service_data=service_data, action_context=action_context)
case _:
logging.error("Not implemented: %s", btype)
def call_ha_service(entity_id, service, service_data = {}):
def call_ha_service(entity_id, service, service_data=None, action_context=None):
if service_data is None:
service_data = {}
action_context = action_context or {}
etype = entity_id.split(".")[0]
libs.home_assistant.call_service(
ok = libs.home_assistant.call_service(
entity_name=entity_id,
domain=etype,
service=service,
service_data=service_data
)
if ok:
logging.info(
"Panel action forwarded to Home Assistant: panel='%s', action='%s', value='%s', entity='%s', service='%s', data=%s",
action_context.get("panel", "unknown"),
action_context.get("btype", "unknown"),
action_context.get("value"),
entity_id,
service,
service_data,
)
if not ok:
logging.error(
"Home Assistant service call failed: entity='%s', service='%s', data=%s",
entity_id,
service,
service_data,
)
def button_press(entity_id, value):
def button_press(entity_id, value, action_context=None):
etype = entity_id.split(".")[0]
match etype:
case 'scene' | 'script':
call_ha_service(entity_id, "turn_on")
call_ha_service(entity_id, "turn_on", action_context=action_context)
case 'light' | 'switch' | 'input_boolean' | 'automation' | 'fan':
call_ha_service(entity_id, "toggle")
call_ha_service(entity_id, "toggle", action_context=action_context)
case 'lock':
state = libs.home_assistant.get_entity_data(entity_id).get('state', '')
if state == "locked":
call_ha_service(entity_id, "unlock")
call_ha_service(entity_id, "unlock", action_context=action_context)
else:
call_ha_service(entity_id, "lock")
call_ha_service(entity_id, "lock", action_context=action_context)
case 'button' | 'input_button':
call_ha_service(entity_id, "press")
call_ha_service(entity_id, "press", action_context=action_context)
case 'input_select' | 'select':
call_ha_service(entity_id, "select_next")
call_ha_service(entity_id, "select_next", action_context=action_context)
case 'vacuum':
state = libs.home_assistant.get_entity_data(entity_id).get('state', '')
if state == "docked":
call_ha_service(entity_id, "start")
call_ha_service(entity_id, "start", action_context=action_context)
else:
call_ha_service(entity_id, "return_to_base")
call_ha_service(entity_id, "return_to_base", action_context=action_context)
case _:
logging.error("buttonpress for entity type %s not implemented", etype)
@@ -274,14 +297,14 @@ def button_press(entity_id, value):
# apis.ha_api.call_service(entity_id.replace(
# 'service.', '', 1).replace('.', '/', 1), **entity_config.data)
def on_off(entity_id, value):
def on_off(entity_id, value, action_context=None):
etype = entity_id.split(".")[0]
match etype:
case 'light' | 'switch' | 'input_boolean' | 'automation' | 'fan':
service = "turn_off"
if value == "1":
service = "turn_on"
call_ha_service(entity_id, service)
call_ha_service(entity_id, service, action_context=action_context)
case _:
logging.error(
"Control action on_off not implemented for %s", entity_id)

View File

@@ -14,8 +14,9 @@ next_id = 0
request_all_states_id = 0
ws_connected = False
home_assistant_entity_state_cache = {}
template_cache = {}
response_buffer = {}
template_cache = {}
response_buffer = {}
nspanel_event_handler = None
ON_CONNECT_HANDLER = None
@@ -44,47 +45,64 @@ def register_on_disconnect_handler(handler):
ON_DISCONNECT_HANDLER = handler
def on_message(ws, message):
global auth_ok, request_all_states_id, home_assistant_entity_state_cache, response_buffer, template_cache
json_msg = json.loads(message)
if json_msg["type"] == "auth_required":
authenticate_client()
elif json_msg["type"] == "auth_ok":
auth_ok = True
logging.info("Home Assistant auth OK. Requesting existing states.")
subscribe_to_events()
_get_all_states()
if ON_CONNECT_HANDLER is not None:
ON_CONNECT_HANDLER()
# for templates
elif json_msg["type"] == "event" and json_msg["id"] in response_buffer:
template_cache[response_buffer[json_msg["id"]]] = {
"result": json_msg["event"]["result"],
"listener-entities": json_msg["event"]["listeners"]["entities"]
}
elif json_msg["type"] == "event" and json_msg["event"]["event_type"] == "state_changed":
entity_id = json_msg["event"]["data"]["entity_id"]
home_assistant_entity_state_cache[entity_id] = json_msg["event"]["data"]["new_state"]
send_entity_update(entity_id)
# rerender template
for template, template_cache_entry in template_cache.items():
if entity_id in template_cache_entry.get("listener-entities", []):
cache_template(template)
elif json_msg["type"] == "event" and json_msg["event"]["event_type"] == "esphome.nspanel.data":
nspanel_data_callback(json_msg["event"]["data"]["device_id"], json_msg["event"]["data"]["CustomRecv"])
elif json_msg["type"] == "result" and not json_msg["success"]:
logging.error("Failed result: ")
logging.error(json_msg)
elif json_msg["type"] == "result" and json_msg["success"]:
if json_msg["id"] == request_all_states_id:
for entity in json_msg["result"]:
home_assistant_entity_state_cache[entity["entity_id"]] = entity
else:
if json_msg["id"] in response_buffer and json_msg.get("result"):
response_buffer[json_msg["id"]] = json_msg["result"]
return None # Ignore success result messages
else:
logging.debug(message)
def on_message(ws, message):
global auth_ok, request_all_states_id, home_assistant_entity_state_cache, response_buffer, template_cache
try:
json_msg = json.loads(message)
except json.JSONDecodeError:
logging.exception("Failed to parse Home Assistant websocket message as JSON")
return
message_type = json_msg.get("type")
if message_type == "auth_required":
authenticate_client()
elif message_type == "auth_ok":
auth_ok = True
logging.info("Home Assistant auth OK. Requesting existing states.")
subscribe_to_events()
_get_all_states()
if ON_CONNECT_HANDLER is not None:
ON_CONNECT_HANDLER()
# for templates
elif message_type == "event" and json_msg.get("id") in response_buffer:
event = json_msg.get("event", {})
listeners = event.get("listeners", {})
template_cache[response_buffer[json_msg["id"]]] = {
"result": event.get("result"),
"listener-entities": listeners.get("entities", [])
}
elif message_type == "event" and json_msg.get("event", {}).get("event_type") == "state_changed":
event_data = json_msg.get("event", {}).get("data", {})
entity_id = event_data.get("entity_id")
if not entity_id:
logging.debug("Received state_changed event without entity_id")
return
home_assistant_entity_state_cache[entity_id] = event_data.get("new_state")
send_entity_update(entity_id)
# rerender template
for template, template_cache_entry in template_cache.items():
if entity_id in template_cache_entry.get("listener-entities", []):
cache_template(template)
elif message_type == "event" and json_msg.get("event", {}).get("event_type") == "esphome.nspanel.data":
event_data = json_msg.get("event", {}).get("data", {})
device_id = event_data.get("device_id")
custom_recv = event_data.get("CustomRecv")
if nspanel_event_handler is None:
logging.debug("No NsPanel event handler registered; dropping event for device '%s'", device_id)
return
nspanel_event_handler(device_id, custom_recv)
elif message_type == "result" and not json_msg.get("success"):
logging.error("Home Assistant request failed: %s", json_msg)
elif message_type == "result" and json_msg.get("success"):
if json_msg.get("id") == request_all_states_id:
for entity in json_msg.get("result", []):
home_assistant_entity_state_cache[entity["entity_id"]] = entity
else:
if json_msg.get("id") in response_buffer and json_msg.get("result"):
response_buffer[json_msg["id"]] = json_msg["result"]
return None # Ignore success result messages
else:
logging.debug(message)
def _ws_connection_open(ws):
@@ -95,20 +113,24 @@ def _ws_connection_open(ws):
ON_CONNECT_HANDLER()
def _ws_connection_close(ws, close_status_code, close_msg):
global ws_connected
ws_connected = False
logging.error("WebSocket connection closed!")
if ON_DISCONNECT_HANDLER is not None:
ON_DISCONNECT_HANDLER()
def _ws_connection_close(ws, close_status_code, close_msg):
global ws_connected
ws_connected = False
logging.error(
"WebSocket connection closed (status=%s, message=%s)",
close_status_code,
close_msg,
)
if ON_DISCONNECT_HANDLER is not None:
ON_DISCONNECT_HANDLER()
def connect():
Thread(target=_do_connection, daemon=True).start()
def _do_connection():
global home_assistant_url, ws, settings
def _do_connection():
global home_assistant_url, ws, settings
ws_url = home_assistant_url.replace(
"https://", "wss://").replace("http://", "ws://")
if settings["is_addon"]:
@@ -117,12 +139,15 @@ def _do_connection():
ws_url += "/api/websocket"
ws = websocket.WebSocketApp(F"{ws_url}", on_message=on_message,
on_open=_ws_connection_open, on_close=_ws_connection_close)
while True:
logging.info(F"Connecting to Home Assistant at {ws_url}")
ws.close()
time.sleep(1)
ws.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE})
time.sleep(10)
while True:
logging.info(F"Connecting to Home Assistant at {ws_url}")
try:
ws.close()
time.sleep(1)
ws.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE})
except Exception:
logging.exception("WebSocket connection loop failed")
time.sleep(10)
def authenticate_client():
@@ -144,9 +169,9 @@ def subscribe_to_events():
}
send_message(json.dumps(msg))
def subscribe_to_nspanel_events(nsp_callback):
global next_id, nspanel_data_callback
nspanel_data_callback = nsp_callback
def subscribe_to_nspanel_events(nsp_callback):
global next_id, nspanel_event_handler
nspanel_event_handler = nsp_callback
msg = {
"id": next_id,
"type": "subscribe_events",
@@ -168,11 +193,13 @@ def send_entity_update(entity_id):
global on_ha_update
on_ha_update(entity_id)
def nspanel_data_callback(device_id, msg):
global nspanel_data_callback
nspanel_data_callback(device_id, msg)
def nspanel_data_callback(device_id, msg):
if nspanel_event_handler is None:
logging.debug("NsPanel callback invoked before handler was registered")
return
nspanel_event_handler(device_id, msg)
def call_service(entity_name: str, domain: str, service: str, service_data: dict) -> bool:
def call_service(entity_name: str, domain: str, service: str, service_data: dict) -> bool:
global next_id
try:
msg = {
@@ -187,9 +214,12 @@ def call_service(entity_name: str, domain: str, service: str, service_data: dict
}
send_message(json.dumps(msg))
return True
except Exception as e:
logging.exception("Failed to call Home Assisatant service.")
return False
except Exception:
logging.exception(
"Failed to call Home Assistant service: %s.%s for %s",
domain, service, entity_name
)
return False
def send_msg_to_panel(service: str, service_data: dict) -> bool:
global next_id
@@ -203,9 +233,9 @@ def send_msg_to_panel(service: str, service_data: dict) -> bool:
}
send_message(json.dumps(msg))
return True
except Exception as e:
logging.exception("Failed to call Home Assisatant service.")
return False
except Exception:
logging.exception("Failed to call Home Assistant panel service: %s", service)
return False
def execute_script(entity_name: str, domain: str, service: str, service_data: dict) -> str:
global next_id, response_buffer
@@ -241,13 +271,13 @@ def execute_script(entity_name: str, domain: str, service: str, service_data: di
else:
return response_buffer[call_id]["response"]
raise TimeoutError("Did not recive respose in time to HA script call")
except Exception as e:
logging.exception("Failed to call Home Assisatant script.")
return {}
except Exception:
logging.exception("Failed to call Home Assistant script: %s.%s", domain, service)
return {}
def cache_template(template):
if not template:
raise Exception("Invalid template")
def cache_template(template):
if not template:
raise ValueError("Invalid template")
global next_id, response_buffer
try:
call_id = next_id
@@ -259,9 +289,9 @@ def cache_template(template):
}
send_message(json.dumps(msg))
return True
except Exception as e:
logging.exception("Failed to render template.")
return False
except Exception:
logging.exception("Failed to render template.")
return False
def get_template(template):
global template_cache
@@ -299,7 +329,12 @@ def is_existent(entity_id: str):
return False
def send_message(message):
global ws, next_id
next_id += 1
ws.send(message)
def send_message(message):
global ws, next_id
try:
next_id += 1
ws.send(message)
except NameError:
logging.error("WebSocket client is not initialized; dropping outgoing message")
except Exception:
logging.exception("Failed sending websocket message to Home Assistant")

View File

@@ -25,53 +25,76 @@ last_settings_file_mtime = 0
mqtt_connect_time = 0
has_sent_reload_command = False
logging.basicConfig(level=logging.DEBUG)
log_level_name = os.getenv("LOGLEVEL", "DEBUG").upper()
log_level = getattr(logging, log_level_name, None)
invalid_log_level = not isinstance(log_level, int)
if invalid_log_level:
log_level = logging.DEBUG
logging.basicConfig(
level=log_level,
format="%(asctime)s %(levelname)s [%(threadName)s] %(name)s: %(message)s",
)
if invalid_log_level:
logging.warning("Invalid loglevel '%s', defaulting to DEBUG", log_level_name)
def on_ha_update(entity_id):
global panel_in_queues
# send HA updates to all panels
for queue in panel_in_queues.values():
queue.put(("HA:", entity_id))
try:
queue.put(("HA:", entity_id))
except Exception:
logging.exception("Failed to enqueue HA update for entity '%s'", entity_id)
def on_ha_panel_event(device_id, msg):
global panel_in_queues
if device_id in panel_in_queues.keys():
queue = panel_in_queues[device_id]
queue.put(("MQTT:", msg))
try:
queue.put(("MQTT:", msg))
except Exception:
logging.exception("Failed to enqueue panel event for device '%s'", device_id)
def process_output_to_panel():
while True:
msg = panel_out_queue.get()
#client.publish(msg[0], msg[1])
#apis.ha_api.call_service(service="esphome/" + self._api_panel_name + "_nspanelui_api_call", command=2, data=msg)
service = msg[0] + "_nspanelui_api_call"
service_data = {
"data": msg[1],
"command":2
try:
msg = panel_out_queue.get()
service = msg[0] + "_nspanelui_api_call"
service_data = {
"data": msg[1],
"command": 2
}
libs.home_assistant.send_msg_to_panel(
service = service,
service_data = service_data
)
libs.home_assistant.send_msg_to_panel(
service=service,
service_data=service_data
)
except Exception:
logging.exception("Failed to process outgoing panel message")
def connect():
global settings, panel_out_queue
ha_is_configured = settings["home_assistant_address"] != "" and settings["home_assistant_token"] != ""
if "mqtt_server" in settings and not "use_ha_api" in settings:
MqttManager(settings, panel_out_queue, panel_in_queues)
else:
logging.info("MQTT values not configured, will not connect.")
# MQTT Connected, start APIs if configured
if settings["home_assistant_address"] != "" and settings["home_assistant_token"] != "":
if ha_is_configured:
libs.home_assistant.init(settings, on_ha_update)
libs.home_assistant.connect()
else:
logging.info("Home Assistant values not configured, will not connect.")
return
wait_seconds = 0
while not libs.home_assistant.ws_connected:
wait_seconds += 1
if wait_seconds % 10 == 0:
logging.info("Waiting for Home Assistant websocket connection... (%ss)", wait_seconds)
time.sleep(1)
if settings.get("use_ha_api"):
libs.home_assistant.subscribe_to_nspanel_events(on_ha_panel_event)
@@ -97,13 +120,20 @@ def setup_panels():
panel_thread.start()
def panel_thread_target(queue_in, name, settings_panel, queue_out):
panel = LovelaceUIPanel(name, settings_panel, queue_out)
try:
panel = LovelaceUIPanel(name, settings_panel, queue_out)
except Exception:
logging.exception("Failed to initialize panel thread for '%s'", name)
return
while True:
msg = queue_in.get()
if msg[0] == "MQTT:":
panel.customrecv_event_callback(msg[1])
elif msg[0] == "HA:":
panel.ha_event_callback(msg[1])
try:
msg = queue_in.get()
if msg[0] == "MQTT:":
panel.customrecv_event_callback(msg[1])
elif msg[0] == "HA:":
panel.ha_event_callback(msg[1])
except Exception:
logging.exception("Panel thread '%s' failed while handling queue message", name)
def get_config_file():
CONFIG_FILE = os.getenv('CONFIG_FILE')
@@ -117,18 +147,27 @@ def get_config(file):
try:
with open(file, 'r', encoding="utf8") as file:
settings = yaml.safe_load(file)
except FileNotFoundError:
logging.error("Config file not found: %s", file)
return False
except OSError:
logging.exception("Failed reading config file: %s", file)
return False
except yaml.YAMLError as exc:
print ("Error while parsing YAML file:")
logging.error("Error while parsing YAML file: %s", file)
if hasattr(exc, 'problem_mark'):
if exc.context != None:
print (' parser says\n' + str(exc.problem_mark) + '\n ' +
str(exc.problem) + ' ' + str(exc.context) +
'\nPlease correct data and retry.')
logging.error(
"Parser says\n%s\n%s %s\nPlease correct data and retry.",
str(exc.problem_mark), str(exc.problem), str(exc.context)
)
else:
print (' parser says\n' + str(exc.problem_mark) + '\n ' +
str(exc.problem) + '\nPlease correct data and retry.')
logging.error(
"Parser says\n%s\n%s\nPlease correct data and retry.",
str(exc.problem_mark), str(exc.problem)
)
else:
print ("Something went wrong while parsing yaml file")
logging.exception("Something went wrong while parsing yaml file")
return False
if not settings.get("mqtt_username"):
@@ -172,10 +211,14 @@ def config_watch():
project_files.append(get_config_file())
handler = ConfigChangeEventHandler(project_files)
observer = Observer()
observer.schedule(handler, path=os.path.dirname(get_config_file()), recursive=True)
watch_path = os.path.dirname(get_config_file()) or "."
observer.schedule(handler, path=watch_path, recursive=True)
observer.start()
while True:
time.sleep(1)
try:
time.sleep(1)
except Exception:
logging.exception("Config watch loop failed")
def signal_handler(signum, frame):
logging.info(f"Received signal {signum}. Initiating restart...")
@@ -194,4 +237,5 @@ if __name__ == '__main__':
time.sleep(100)
else:
while True:
time.sleep(100)
time.sleep(100)

View File

@@ -19,7 +19,6 @@ class MqttManager:
self.client.username_pw_set(
settings["mqtt_username"], settings["mqtt_password"])
# Wait for connection
connection_return_code = 0
mqtt_server = settings["mqtt_server"]
mqtt_port = int(settings["mqtt_port"])
logging.info("Connecting to %s:%i as %s",
@@ -28,9 +27,12 @@ class MqttManager:
try:
self.client.connect(mqtt_server, mqtt_port, 5)
break # Connection call did not raise exception, connection is sucessfull
except: # pylint: disable=bare-except
except Exception: # pylint: disable=broad-exception-caught
logging.exception(
"Failed to connect to MQTT %s:%i. Will try again in 10 seconds. Code: %s", mqtt_server, mqtt_port, connection_return_code)
"Failed to connect to MQTT %s:%i. Will try again in 10 seconds.",
mqtt_server,
mqtt_port,
)
time.sleep(10.)
self.client.loop_start()
process_thread = threading.Thread(target=self.process_in_queue, args=(self.client, self.msg_in_queue))
@@ -38,31 +40,52 @@ class MqttManager:
process_thread.start()
def on_mqtt_connect(self, client, userdata, flags, rc):
if rc != 0:
logging.error("MQTT connection failed with return code: %s", rc)
return
logging.info("Connected to MQTT Server")
# subscribe to panelRecvTopic of each panel
for settings_panel in self.settings["nspanels"].values():
client.subscribe(settings_panel["panelRecvTopic"])
topic = settings_panel["panelRecvTopic"]
result, _ = client.subscribe(topic)
if result == mqtt.MQTT_ERR_SUCCESS:
logging.debug("Subscribed to panel topic: %s", topic)
else:
logging.error("Failed to subscribe to panel topic '%s' (result=%s)", topic, result)
def on_mqtt_message(self, client, userdata, msg):
try:
if msg.payload.decode() == "":
payload_text = msg.payload.decode('utf-8')
if payload_text == "":
logging.debug("Ignoring empty MQTT payload on topic: %s", msg.topic)
return
if msg.topic in self.msg_out_queue_list.keys():
data = json.loads(msg.payload.decode('utf-8'))
data = json.loads(payload_text)
if "CustomRecv" in data:
queue = self.msg_out_queue_list[msg.topic]
queue.put(("MQTT:", data["CustomRecv"]))
else:
logging.debug("JSON payload on topic '%s' has no 'CustomRecv' key", msg.topic)
else:
logging.debug("Received unhandled message on topic: %s", msg.topic)
except UnicodeDecodeError:
logging.exception("Failed to decode MQTT payload as UTF-8 on topic: %s", msg.topic)
except json.JSONDecodeError:
logging.exception("Failed to parse MQTT JSON payload on topic: %s", msg.topic)
except Exception: # pylint: disable=broad-exception-caught
logging.exception("Something went wrong during processing of message:")
logging.exception("Unexpected error while processing MQTT message on topic: %s", msg.topic)
try:
logging.error(msg.payload.decode('utf-8'))
except: # pylint: disable=bare-except
except Exception: # pylint: disable=broad-exception-caught
logging.error(
"Something went wrong when processing the exception message, couldn't decode payload to utf-8.")
def process_in_queue(self, client, msg_in_queue):
while True:
msg = msg_in_queue.get()
client.publish(msg[0], msg[1])
try:
msg = msg_in_queue.get()
result = client.publish(msg[0], msg[1])
if result.rc != mqtt.MQTT_ERR_SUCCESS:
logging.error("Failed publishing message to topic '%s' (rc=%s)", msg[0], result.rc)
except Exception: # pylint: disable=broad-exception-caught
logging.exception("Failed processing outgoing MQTT queue message")

View File

@@ -103,10 +103,13 @@ class LovelaceUIPanel:
libs.panel_cmd.page_type(self.msg_out_queue, self.sendTopic, "pageStartup")
def schedule_thread_target(self):
while True:
self.schedule.exec_jobs()
time.sleep(1)
def schedule_thread_target(self):
while True:
try:
self.schedule.exec_jobs()
except Exception:
logging.exception("Scheduler execution failed for panel '%s'", self.name)
time.sleep(1)
def update_time(self):
use_timezone = tz.gettz(self.settings["timeZone"])
@@ -200,12 +203,15 @@ class LovelaceUIPanel:
return card
return list(self.cards.values())[0]
def customrecv_event_callback(self, msg):
logging.debug("Recv Message from NsPanel (%s): %s", self.name, msg)
msg = msg.split(",")
# run action based on received command
if msg[0] == "event":
if msg[1] == "startup":
def customrecv_event_callback(self, msg):
logging.debug("Recv Message from NsPanel (%s): %s", self.name, msg)
msg = msg.split(",")
if len(msg) < 2:
logging.error("Malformed panel message on '%s': %s", self.name, msg)
return
# run action based on received command
if msg[0] == "event":
if msg[1] == "startup":
# TODO: Handle Update Messages
self.update_date()
self.update_time()
@@ -226,15 +232,24 @@ class LovelaceUIPanel:
self.render_current_page(switchPages=True)
if msg[1] == "renderCurrentPage":
self.render_current_page(requested=True)
if msg[1] == "buttonPress2":
entity_id = msg[2]
if entity_id == "":
return
btype = msg[3]
value = msg[4] if len(msg) > 4 else None
if btype == "bExit":
if entity_id in ["screensaver", "screensaver2"] and self.settings.get("screensaver").get("doubleTapToUnlock") and value == "1":
return
if msg[1] == "buttonPress2":
if len(msg) < 4:
logging.error("Malformed buttonPress2 payload on '%s': %s", self.name, msg)
return
entity_id = msg[2]
if entity_id == "":
return
btype = msg[3]
value = msg[4] if len(msg) > 4 else None
entity_config = {}
action_context = {
"panel": self.name,
"btype": btype,
"value": value,
}
if btype == "bExit":
if entity_id in ["screensaver", "screensaver2"] and self.settings.get("screensaver").get("doubleTapToUnlock") and value == "1":
return
# in case privious_cards is empty add a default card
if len(self.privious_cards) == 0:
@@ -249,12 +264,12 @@ class LovelaceUIPanel:
return
# replace iid with real entity id
if entity_id.startswith("iid."):
iid = entity_id.split(".")[1]
for e in self.current_card.entities:
if e.iid == iid:
entity_id = e.entity_id
entity_config = e.config
if entity_id.startswith("iid."):
iid = entity_id.split(".")[1]
for e in self.current_card.entities:
if e.iid == iid:
entity_id = e.entity_id
entity_config = e.config
match btype:
case 'button':
@@ -271,31 +286,51 @@ class LovelaceUIPanel:
self.privious_cards.append(self.current_card)
self.current_card = self.searchCard(card_iid)
self.render_current_page(switchPages=True)
# send ha stuff to ha
case _:
ha_control.handle_buttons(entity_id, btype, value, entity_config=entity_config)
# send ha stuff to ha
case _:
ha_control.handle_buttons(
entity_id,
btype,
value,
entity_config=entity_config,
action_context=action_context,
)
case 'cardUnlock-unlock':
card_iid = entity_id.split(".")[1]
if int(self.current_card.config.get("pin")) == int(value):
self.privious_cards.append(self.current_card)
self.current_card = self.searchCard(card_iid)
self.render_current_page(switchPages=True)
case 'mode-light':
ha_control.handle_buttons(entity_id, btype, value, entity_config=entity_config)
case _:
ha_control.handle_buttons(entity_id, btype, value)
case 'mode-light':
ha_control.handle_buttons(
entity_id,
btype,
value,
entity_config=entity_config,
action_context=action_context,
)
case _:
ha_control.handle_buttons(
entity_id,
btype,
value,
action_context=action_context,
)
if msg[1] == "pageOpenDetail":
entity_id = msg[3]
# replace iid with real entity id
if entity_id.startswith("iid."):
iid = entity_id.split(".")[1]
for e in self.current_card.entities:
if e.iid == iid:
entity_id = e.entity_id
effectList = None
if entity_id.startswith("light"):
effectList = e.config.get("effectList")
if msg[1] == "pageOpenDetail":
if len(msg) < 4:
logging.error("Malformed pageOpenDetail payload on '%s': %s", self.name, msg)
return
entity_id = msg[3]
effectList = None
# replace iid with real entity id
if entity_id.startswith("iid."):
iid = entity_id.split(".")[1]
for e in self.current_card.entities:
if e.iid == iid:
entity_id = e.entity_id
if entity_id.startswith("light"):
effectList = e.config.get("effectList")
if msg[2] == "popupInSel": #entity_id.split(".")[0] in ['input_select', 'media_player']:
libs.panel_cmd.entityUpdateDetail2(self.msg_out_queue, self.sendTopic, detail_open(self.settings["locale"], msg[2], entity_id, msg[3], self.msg_out_queue, sendTopic=self.sendTopic, options_list=effectList))
else: