mirror of
https://github.com/joBr99/nspanel-lovelace-ui.git
synced 2026-02-22 14:08:37 +01:00
Compare commits
32 Commits
b6f36d4eac
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d745d63def | ||
|
|
101e69bbde | ||
|
|
d55a63b07c | ||
|
|
52a041521d | ||
|
|
f7fa1653c6 | ||
|
|
b925ea8a2d | ||
|
|
da095b1591 | ||
|
|
ca6e271a54 | ||
|
|
698ad7c771 | ||
|
|
b0f40a1a87 | ||
|
|
2cf7bcb63e | ||
|
|
05d9ff52ca | ||
|
|
bb79cd8f85 | ||
|
|
00f7119cd2 | ||
|
|
8e6f923839 | ||
|
|
cf396b0259 | ||
|
|
42c27a3794 | ||
|
|
c65f1935e5 | ||
|
|
17284d83ca | ||
|
|
2853073a59 | ||
|
|
8de034adf9 | ||
|
|
330fa9bfd4 | ||
|
|
ec971f5f3e | ||
|
|
41f4062ab8 | ||
|
|
f3fffe7b70 | ||
|
|
c4b6a8bd8a | ||
|
|
81d876b53b | ||
|
|
d15cb218ce | ||
|
|
114f630b8a | ||
|
|
53b627be88 | ||
|
|
f2e1a7263d | ||
|
|
1e2f89ed1d |
6
.github/workflows/builder.yaml
vendored
6
.github/workflows/builder.yaml
vendored
@@ -21,7 +21,7 @@ jobs:
|
|||||||
changed: ${{ steps.changed_addons.outputs.changed }}
|
changed: ${{ steps.changed_addons.outputs.changed }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out the repository
|
- name: Check out the repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed_files
|
id: changed_files
|
||||||
@@ -68,7 +68,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repository
|
- name: Check out repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Get information
|
- name: Get information
|
||||||
id: info
|
id: info
|
||||||
@@ -92,7 +92,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
if: env.BUILD_ARGS != '--test'
|
if: env.BUILD_ARGS != '--test'
|
||||||
uses: docker/login-action@v3.6.0
|
uses: docker/login-action@v3.7.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
|
|||||||
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
@@ -43,11 +43,11 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3
|
uses: github/codeql-action/init@v4
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# 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).
|
# 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)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v3
|
uses: github/codeql-action/autobuild@v4
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 https://git.io/JvXDl
|
# 📚 https://git.io/JvXDl
|
||||||
@@ -72,4 +72,4 @@ jobs:
|
|||||||
# make release
|
# make release
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v3
|
uses: github/codeql-action/analyze@v4
|
||||||
|
|||||||
28
.github/workflows/docs-dev.yml
vendored
28
.github/workflows/docs-dev.yml
vendored
@@ -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
|
|
||||||
41
.github/workflows/docs-release.yml
vendored
41
.github/workflows/docs-release.yml
vendored
@@ -6,24 +6,51 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths:
|
paths:
|
||||||
- docs/*
|
- docs/**
|
||||||
|
- docs-standalone/**
|
||||||
- .github/workflows/docs-release.yml
|
- .github/workflows/docs-release.yml
|
||||||
- mkdocs.yml
|
- mkdocs.yml
|
||||||
|
- docs-standalone/mkdocs.yml
|
||||||
- HMI/README.md
|
- HMI/README.md
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- uses: actions/setup-python@v6
|
- uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: 3.x
|
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: cp HMI/README.md docs/hmi-serial-protocol.md
|
||||||
- run: git config --global user.name Docs deploy
|
- run: zensical build --config-file mkdocs.yml
|
||||||
- run: git config --global user.email docs@dummy.bot.com
|
- run: mv site _site_main
|
||||||
- run: mike set-default stable
|
- run: zensical build --config-file docs-standalone/mkdocs.yml
|
||||||
- run: mike deploy --push --update-aliases stable
|
- 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
|
||||||
|
|||||||
2
.github/workflows/hacs-validation.yaml
vendored
2
.github/workflows/hacs-validation.yaml
vendored
@@ -11,7 +11,7 @@ jobs:
|
|||||||
name: HACS Action
|
name: HACS Action
|
||||||
runs-on: "ubuntu-latest"
|
runs-on: "ubuntu-latest"
|
||||||
steps:
|
steps:
|
||||||
- uses: "actions/checkout@v5"
|
- uses: "actions/checkout@v6"
|
||||||
- name: HACS Action
|
- name: HACS Action
|
||||||
uses: "hacs/action@main"
|
uses: "hacs/action@main"
|
||||||
with:
|
with:
|
||||||
|
|||||||
2
.github/workflows/iobroker-localization.yml
vendored
2
.github/workflows/iobroker-localization.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
|||||||
gen-ioBroker-localization:
|
gen-ioBroker-localization:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.head_ref }}
|
ref: ${{ github.head_ref }}
|
||||||
|
|
||||||
|
|||||||
6
.github/workflows/lint.yaml
vendored
6
.github/workflows/lint.yaml
vendored
@@ -18,7 +18,7 @@ jobs:
|
|||||||
addons: ${{ steps.addons.outputs.addons_list }}
|
addons: ${{ steps.addons.outputs.addons_list }}
|
||||||
steps:
|
steps:
|
||||||
- name: ⤵️ Check out code from GitHub
|
- name: ⤵️ Check out code from GitHub
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: 🔍 Find add-on directories
|
- name: 🔍 Find add-on directories
|
||||||
id: addons
|
id: addons
|
||||||
@@ -33,9 +33,9 @@ jobs:
|
|||||||
path: ${{ fromJson(needs.find.outputs.addons) }}
|
path: ${{ fromJson(needs.find.outputs.addons) }}
|
||||||
steps:
|
steps:
|
||||||
- name: ⤵️ Check out code from GitHub
|
- name: ⤵️ Check out code from GitHub
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: 🚀 Run Home Assistant Add-on Lint
|
- name: 🚀 Run Home Assistant Add-on Lint
|
||||||
uses: frenck/action-addon-linter@v2.18
|
uses: frenck/action-addon-linter@v2.21
|
||||||
with:
|
with:
|
||||||
path: "./${{ matrix.path }}"
|
path: "./${{ matrix.path }}"
|
||||||
|
|||||||
2
.github/workflows/nextion2text.yml
vendored
2
.github/workflows/nextion2text.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.head_ref }}
|
ref: ${{ github.head_ref }}
|
||||||
|
|
||||||
|
|||||||
29
.gitignore
vendored
29
.gitignore
vendored
@@ -6,4 +6,31 @@ HMI/Nextion2Text.py
|
|||||||
panels.yaml
|
panels.yaml
|
||||||
|
|
||||||
# don't add Webstorm project stuff
|
# 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
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ If you like this project consider buying me a pizza 🍕 <a href="https://paypal
|
|||||||
[](https://github.com/hacs/integration)
|
[](https://github.com/hacs/integration)
|
||||||

|

|
||||||
[](https://github.com/joBr99/nspanel-lovelace-ui/releases)
|
[](https://github.com/joBr99/nspanel-lovelace-ui/releases)
|
||||||

|

|
||||||
[](https://github.com/joBr99/nspanel-lovelace-ui/commits/main)
|
[](https://github.com/joBr99/nspanel-lovelace-ui/commits/main)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -206,11 +206,15 @@ class LuiController(object):
|
|||||||
self._pages_gen.generate_timer_detail_page(entity_id, True)
|
self._pages_gen.generate_timer_detail_page(entity_id, True)
|
||||||
|
|
||||||
def button_press(self, entity_id, button_type, value):
|
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'):
|
if entity_id.startswith('uuid'):
|
||||||
entity_config = self._config._config_entites_table.get(entity_id)
|
entity_config = self._config._config_entites_table.get(entity_id)
|
||||||
if entity_config is not None:
|
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
|
# internal buttons
|
||||||
if entity_id == "screensaver" and button_type == "bExit":
|
if entity_id == "screensaver" and button_type == "bExit":
|
||||||
# get default card if there is one
|
# get default card if there is one
|
||||||
|
|||||||
112
docs-standalone/docs/_assets/user.css
Normal file
112
docs-standalone/docs/_assets/user.css
Normal 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;
|
||||||
|
}
|
||||||
137
docs-standalone/docs/cards.md
Normal file
137
docs-standalone/docs/cards.md
Normal 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
|
||||||
|
```
|
||||||
61
docs-standalone/docs/configuration.md
Normal file
61
docs-standalone/docs/configuration.md
Normal 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
|
||||||
|
```
|
||||||
45
docs-standalone/docs/connection-modes.md
Normal file
45
docs-standalone/docs/connection-modes.md
Normal 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.
|
||||||
77
docs-standalone/docs/entities.md
Normal file
77
docs-standalone/docs/entities.md
Normal 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]
|
||||||
|
```
|
||||||
55
docs-standalone/docs/getting-started.md
Normal file
55
docs-standalone/docs/getting-started.md
Normal 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`
|
||||||
38
docs-standalone/docs/index.md
Normal file
38
docs-standalone/docs/index.md
Normal 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.
|
||||||
120
docs-standalone/docs/migration-appdaemon.md
Normal file
120
docs-standalone/docs/migration-appdaemon.md
Normal 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.
|
||||||
57
docs-standalone/docs/screensaver.md
Normal file
57
docs-standalone/docs/screensaver.md
Normal 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
|
||||||
|
```
|
||||||
63
docs-standalone/docs/troubleshooting.md
Normal file
63
docs-standalone/docs/troubleshooting.md
Normal 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>`
|
||||||
60
docs-standalone/mkdocs.yml
Normal file
60
docs-standalone/mkdocs.yml
Normal 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
|
||||||
@@ -10,14 +10,46 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*.md-header,*/ .md-footer {
|
/*.md-header,*/ .md-footer,
|
||||||
|
.md-footer-meta {
|
||||||
background-color: #333333;
|
background-color: #333333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.md-footer__inner.md-grid {
|
/* Footer contrast fixes for Zensical/Material variants */
|
||||||
display: none;
|
: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 {
|
.md-sidebar {
|
||||||
padding-top: 0px;
|
padding-top: 0px;
|
||||||
}
|
}
|
||||||
@@ -77,4 +109,4 @@
|
|||||||
|
|
||||||
ol li::marker {
|
ol li::marker {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|||||||
75
docs/config-migration-standalone.md
Normal file
75
docs/config-migration-standalone.md
Normal 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/)
|
||||||
@@ -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
|
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 / BerryDriver 10 / Tasmota 15.2.0
|
- abgestimmt auf TFT 61 / v5.1.1 (v5.1.2 us-p) / BerryDriver 10 / Tasmota 15.2.0
|
||||||
|
|
||||||
Projekt:
|
Projekt:
|
||||||
https://github.com/joBr99/nspanel-lovelace-ui/tree/main/ioBroker
|
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 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.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)
|
- 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! *
|
* 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! *
|
* 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 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-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 _____________________________________
|
// _________________________________ DE: Ab hier keine Konfiguration mehr _____________________________________
|
||||||
// _________________________________ EN: No more configuration from here _____________________________________
|
// _________________________________ 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 tft_version: string = 'v5.1.1';
|
||||||
const desired_display_firmware_version = 61;
|
const desired_display_firmware_version = 61;
|
||||||
const berry_driver_version = 10;
|
const berry_driver_version = 10;
|
||||||
@@ -8849,7 +8852,11 @@ function unsubscribePowerSubscriptions (): void {
|
|||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
function subscribePowerSubscriptions (id: string): 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 () {
|
(function () {
|
||||||
if (timeoutPower) {
|
if (timeoutPower) {
|
||||||
clearTimeout(timeoutPower);
|
clearTimeout(timeoutPower);
|
||||||
@@ -8862,7 +8869,6 @@ function subscribePowerSubscriptions (id: string): void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @function GeneratePowerPage
|
* @function GeneratePowerPage
|
||||||
* @description Generates a page with power state and energy usage information.
|
* @description Generates a page with power state and energy usage information.
|
||||||
@@ -10188,6 +10194,10 @@ function HandleButtonEvent (words: any): void {
|
|||||||
break;
|
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;
|
break;
|
||||||
case 'mode-seek':
|
case 'mode-seek':
|
||||||
@@ -11448,7 +11458,7 @@ function GenerateDetailPage (type: NSPanel.PopupType, optional: NSPanel.mediaOpt
|
|||||||
let hSlider2MinVal: number = pageItem.minValue ?? 0;
|
let hSlider2MinVal: number = pageItem.minValue ?? 0;
|
||||||
let hSlider2MaxVal: number = pageItem.maxValue ?? 100;
|
let hSlider2MaxVal: number = pageItem.maxValue ?? 100;
|
||||||
let hSlider2ZeroVal: number = 0;
|
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 hSlider2Step: number = 1;
|
||||||
let hSlider2Visibility: string = "enable";
|
let hSlider2Visibility: string = "enable";
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
TypeScript v5.1.1.4 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
|
- abgestimmt auf TFT 61 / v5.1.1 (v5.1.2 us-p) / BerryDriver 10 / Tasmota 15.2.0
|
||||||
|
|
||||||
Projekt:
|
Projekt:
|
||||||
https://github.com/joBr99/nspanel-lovelace-ui/tree/main/ioBroker
|
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
|
- 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.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
|
- 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! *
|
* 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! *
|
* 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:
|
Erforderliche Adapter:
|
||||||
|
|
||||||
Pirate-Weather oder BrightSky oder OpenWeatherMap --> Bei Nutzung der Wetterfunktionen (und zur Icon-Konvertierung) im Screensaver
|
Bei Nutzung der Wetterfunktionen (und zur Icon-Konvertierung) im Screensaver einen der folgenden Wetter-Adapter:
|
||||||
!!!DasWetter deprecated - Dienst nur noch für ältere Accounts funktional
|
- Pirate-Weather
|
||||||
!!!AccuWeather deprecated - Dienst schaltet Free-Account ab!!!
|
- 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
|
Alexa2: - Bei Nutzung der dynamischen SpeakerList in der cardMedia
|
||||||
Geräte verwalten - Für Erstellung der Aliase
|
Geräte verwalten - Für Erstellung der Aliase
|
||||||
MQTT-Adapter - Für Kommunikation zwischen Skript und Tasmota
|
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 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
|
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-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.0.2.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 *****/
|
/***** 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)
|
// 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 'daswetter.0.' (deprecated) or '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.';
|
const weatherAdapterInstance: string = 'pirate-weather.0.';
|
||||||
|
|
||||||
// DE: Mögliche Werte: 'Min', 'Max' oder 'MinMax' im Screensaver
|
// DE: Mögliche Werte: 'Min', 'Max' oder 'MinMax' im Screensaver
|
||||||
@@ -992,9 +1005,9 @@ export const config: Config = {
|
|||||||
// _________________________________ DE: Ab hier keine Konfiguration mehr _____________________________________
|
// _________________________________ DE: Ab hier keine Konfiguration mehr _____________________________________
|
||||||
// _________________________________ EN: No more configuration from here _____________________________________
|
// _________________________________ EN: No more configuration from here _____________________________________
|
||||||
|
|
||||||
const scriptVersion: string = 'v5.0.2.1';
|
const scriptVersion: string = 'v5.1.1.4';
|
||||||
const tft_version: string = 'v5.0.2';
|
const tft_version: string = 'v5.1.1';
|
||||||
const desired_display_firmware_version = 59;
|
const desired_display_firmware_version = 61;
|
||||||
const berry_driver_version = 10;
|
const berry_driver_version = 10;
|
||||||
|
|
||||||
const tasmotaOtaUrl: string = 'http://ota.tasmota.com/tasmota32/release/';
|
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');
|
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('.');
|
let weatherAdapterInstanceArray: any = weatherAdapterInstance.split('.');
|
||||||
weatherAdapterInstanceNumber = weatherAdapterInstanceArray[1];
|
weatherAdapterInstanceNumber = weatherAdapterInstanceArray[1];
|
||||||
@@ -2572,11 +2590,11 @@ async function CreateWeatherAlias () {
|
|||||||
if (!existsState(config.weatherEntity + '.ICON') && existsState('brightsky.' + weatherAdapterInstanceNumber + '.current.icon')) {
|
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');
|
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: {}});
|
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',
|
type: 'string',
|
||||||
role: 'value',
|
role: 'value',
|
||||||
name: 'ICON',
|
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, {
|
await createAliasAsync(config.weatherEntity + '.TEMP', 'brightsky.' + weatherAdapterInstanceNumber + '.current.temperature', true, {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
@@ -2603,6 +2621,57 @@ async function CreateWeatherAlias () {
|
|||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
log('error at function CreateWeatherAlias brightsky.' + weatherAdapterInstanceNumber + '.: ' + err.message, 'warn');
|
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) {
|
} catch (err: any) {
|
||||||
@@ -3218,6 +3287,8 @@ async function InitPopupNotify () {
|
|||||||
'~' +
|
'~' +
|
||||||
v_popupNotifyButton1TextColor +
|
v_popupNotifyButton1TextColor +
|
||||||
'~' +
|
'~' +
|
||||||
|
'~' + // Fix Button3_Text for Adapter
|
||||||
|
'~' + // Fix Button3_Color for Adapter
|
||||||
getState(popupNotifyButton2Text).val +
|
getState(popupNotifyButton2Text).val +
|
||||||
'~' +
|
'~' +
|
||||||
v_popupNotifyButton2TextColor +
|
v_popupNotifyButton2TextColor +
|
||||||
@@ -4664,6 +4735,7 @@ function HandleMessage (typ: string, method: NSPanel.EventMethod, page: number |
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'buttonPress3':
|
||||||
case 'buttonPress2':
|
case 'buttonPress2':
|
||||||
screensaverEnabled = false;
|
screensaverEnabled = false;
|
||||||
HandleButtonEvent(words);
|
HandleButtonEvent(words);
|
||||||
@@ -5170,7 +5242,7 @@ function CreateEntity (pageItem: PageItem, placeId: number, useColors: boolean =
|
|||||||
val = getState(pageItem.id + '.ACTUAL').val;
|
val = getState(pageItem.id + '.ACTUAL').val;
|
||||||
RegisterEntityWatcher(pageItem.id + '.ACTUAL');
|
RegisterEntityWatcher(pageItem.id + '.ACTUAL');
|
||||||
}
|
}
|
||||||
if (existsState(pageItem.id + '.SET')) {
|
if (existsState(pageItem.id + '.SET') && !existsState(pageItem.id + 'ACTUAL')) {
|
||||||
val = getState(pageItem.id + '.SET').val;
|
val = getState(pageItem.id + '.SET').val;
|
||||||
RegisterEntityWatcher(pageItem.id + '.SET');
|
RegisterEntityWatcher(pageItem.id + '.SET');
|
||||||
}
|
}
|
||||||
@@ -5183,7 +5255,7 @@ function CreateEntity (pageItem: PageItem, placeId: number, useColors: boolean =
|
|||||||
val = getState(pageItem.id + '.ON_SET').val;
|
val = getState(pageItem.id + '.ON_SET').val;
|
||||||
RegisterEntityWatcher(pageItem.id + '.ON_SET');
|
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;
|
val = getState(pageItem.id + '.ON').val;
|
||||||
RegisterEntityWatcher(pageItem.id + '.ON');
|
RegisterEntityWatcher(pageItem.id + '.ON');
|
||||||
}
|
}
|
||||||
@@ -6350,7 +6422,7 @@ function RegisterEntityWatcher (id: string): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
subscriptions[id] = on({id: id, change: 'any'}, (obj) => {
|
subscriptions[id] = on({id: id, change: 'ne'}, (obj) => {
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
if (obj.oldState && obj.oldState.val === obj.state.val && obj.state.ack) {
|
if (obj.oldState && obj.oldState.val === obj.state.val && obj.state.ack) {
|
||||||
return;
|
return;
|
||||||
@@ -8597,7 +8669,7 @@ async function createAutoQRAlias (id: string, dpPath: string) {
|
|||||||
if (autoCreateAlias) {
|
if (autoCreateAlias) {
|
||||||
if (isSetOptionActive) {
|
if (isSetOptionActive) {
|
||||||
if (existsState(dpPath + 'Daten') == false) {
|
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});
|
await createStateAsync(dpPath + 'Switch', false, {type: 'boolean', write: true});
|
||||||
setObject(id, {_id: id, type: 'channel', common: {role: 'switch.mode.wlan', name: 'QR Page'}, native: {}});
|
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'});
|
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 o = getObject(id);
|
||||||
|
|
||||||
let heading = page.heading !== undefined ? page.heading : typeof o.common.name === 'object' ? o.common.name.de : o.common.name;
|
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;
|
let hiddenPWD = false;
|
||||||
if (page.items[0].hidePassword !== undefined && page.items[0].hidePassword == true) {
|
if (page.items[0].hidePassword !== undefined && page.items[0].hidePassword == true) {
|
||||||
hiddenPWD = true;
|
hiddenPWD = true;
|
||||||
@@ -8780,7 +8852,11 @@ function unsubscribePowerSubscriptions (): void {
|
|||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
function subscribePowerSubscriptions (id: string): 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 () {
|
(function () {
|
||||||
if (timeoutPower) {
|
if (timeoutPower) {
|
||||||
clearTimeout(timeoutPower);
|
clearTimeout(timeoutPower);
|
||||||
@@ -8793,7 +8869,6 @@ function subscribePowerSubscriptions (id: string): void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @function GeneratePowerPage
|
* @function GeneratePowerPage
|
||||||
* @description Generates a page with power state and energy usage information.
|
* @description Generates a page with power state and energy usage information.
|
||||||
@@ -9331,10 +9406,10 @@ function HandleButtonEvent (words: any): void {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'notifyAction':
|
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(popupNotifyInternalName, {val: words[2], ack: true});
|
||||||
setState(popupNotifyAction, {val: true, 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(popupNotifyInternalName, {val: words[2], ack: true});
|
||||||
setState(popupNotifyAction, {val: false, ack: true});
|
setState(popupNotifyAction, {val: false, ack: true});
|
||||||
}
|
}
|
||||||
@@ -10119,6 +10194,10 @@ function HandleButtonEvent (words: any): void {
|
|||||||
break;
|
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;
|
break;
|
||||||
case 'mode-seek':
|
case 'mode-seek':
|
||||||
@@ -10434,7 +10513,11 @@ function HandleButtonEvent (words: any): void {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} 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 hSlider2MinVal: number = pageItem.minValue ?? 0;
|
||||||
let hSlider2MaxVal: number = pageItem.maxValue ?? 100;
|
let hSlider2MaxVal: number = pageItem.maxValue ?? 100;
|
||||||
let hSlider2ZeroVal: number = 0;
|
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 hSlider2Step: number = 1;
|
||||||
let hSlider2Visibility: string = "enable";
|
let hSlider2Visibility: string = "enable";
|
||||||
|
|
||||||
@@ -12391,85 +12474,100 @@ function HandleScreensaverUpdate (): void {
|
|||||||
} else if (weatherAdapterInstance == 'openweathermap.' + weatherAdapterInstanceNumber + '.') {
|
} else if (weatherAdapterInstance == 'openweathermap.' + weatherAdapterInstanceNumber + '.') {
|
||||||
entityIcon = Icons.GetIcon(GetOpenWeatherMapIcon(icon));
|
entityIcon = Icons.GetIcon(GetOpenWeatherMapIcon(icon));
|
||||||
entityIconCol = GetOpenWeatherMapIconColor(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 + '.') {
|
} else if (weatherAdapterInstance == 'pirate-weather.' + weatherAdapterInstanceNumber + '.' || weatherAdapterInstance == 'brightsky.' + weatherAdapterInstanceNumber + '.') {
|
||||||
entityIcon = Icons.GetIcon(GetPirateWeatherIcon(icon));
|
entityIcon = Icons.GetIcon(GetPirateWeatherIcon(icon));
|
||||||
entityIconCol = GetPirateWeatherIconColor(icon);
|
entityIconCol = GetPirateWeatherIconColor(icon);
|
||||||
|
} else if (weatherAdapterInstance == 'brightsky.' + weatherAdapterInstanceNumber + '.') {
|
||||||
|
entityIcon = Icons.GetIcon(icon);
|
||||||
|
entityIconCol = GetBrightskyWeatherIconColor(icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
payloadString += '~' + '~' + entityIcon + '~' + entityIconCol + '~' + '~' + optionalValue + '~';
|
payloadString += '~' + '~' + entityIcon + '~' + entityIconCol + '~' + '~' + optionalValue + '~';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3 leftScreensaverEntities
|
// 3 leftScreensaverEntities
|
||||||
if (screensaverAdvanced) {
|
if (screensaverAdvanced) {
|
||||||
let checkpoint = true;
|
let checkpoint = true;
|
||||||
let i = 0;
|
let i = 0;
|
||||||
if (config.leftScreensaverEntity && Array.isArray(config.leftScreensaverEntity) && config.leftScreensaverEntity.length > 0) {
|
if (config.leftScreensaverEntity && Array.isArray(config.leftScreensaverEntity) && config.leftScreensaverEntity.length > 0) {
|
||||||
for (i = 0; i < 3 && i < config.leftScreensaverEntity.length; i++) {
|
for (i = 0; i < 3 && i < config.leftScreensaverEntity.length; i++) {
|
||||||
const leftScreensaverEntity = config.leftScreensaverEntity[i];
|
const leftScreensaverEntity = config.leftScreensaverEntity[i];
|
||||||
if (leftScreensaverEntity === null || leftScreensaverEntity === undefined) {
|
if (leftScreensaverEntity === null || leftScreensaverEntity === undefined) {
|
||||||
checkpoint = false;
|
checkpoint = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
RegisterScreensaverEntityWatcher(leftScreensaverEntity.ScreensaverEntity);
|
RegisterScreensaverEntityWatcher(leftScreensaverEntity.ScreensaverEntity);
|
||||||
|
|
||||||
let val = getState(leftScreensaverEntity.ScreensaverEntity).val;
|
let val = getState(leftScreensaverEntity.ScreensaverEntity).val;
|
||||||
let iconColor = rgb_dec565(White);
|
let iconColor = rgb_dec565(White);
|
||||||
let icon;
|
let icon;
|
||||||
if (typeof leftScreensaverEntity.ScreensaverEntityIconOn == 'string' && existsObject(leftScreensaverEntity.ScreensaverEntityIconOn as string)) {
|
if (typeof leftScreensaverEntity.ScreensaverEntityIconOn == 'string' && existsObject(leftScreensaverEntity.ScreensaverEntityIconOn as string)) {
|
||||||
let iconName = getState(leftScreensaverEntity.ScreensaverEntityIconOn!).val;
|
let iconName = getState(leftScreensaverEntity.ScreensaverEntityIconOn!).val;
|
||||||
icon = Icons.GetIcon(iconName);
|
icon = Icons.GetIcon(iconName);
|
||||||
} else {
|
} else {
|
||||||
icon = Icons.GetIcon(leftScreensaverEntity.ScreensaverEntityIconOn);
|
icon = Icons.GetIcon(leftScreensaverEntity.ScreensaverEntityIconOn);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parseFloat(val + '') == val) {
|
if (parseFloat(val + '') == val) {
|
||||||
val = parseFloat(val);
|
val = parseFloat(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof val == 'number') {
|
if (typeof val == 'number') {
|
||||||
val = val * (leftScreensaverEntity.ScreensaverEntityFactor ? leftScreensaverEntity.ScreensaverEntityFactor! : 0)
|
val = val * (leftScreensaverEntity.ScreensaverEntityFactor ? leftScreensaverEntity.ScreensaverEntityFactor! : 0)
|
||||||
icon = determineScreensaverStatusIcon(leftScreensaverEntity,val,icon)
|
icon = determineScreensaverStatusIcon(leftScreensaverEntity, val, icon)
|
||||||
val = val.toFixed(
|
|
||||||
leftScreensaverEntity.ScreensaverEntityDecimalPlaces
|
// Einheit ermitteln: String oder aus DP
|
||||||
) + leftScreensaverEntity.ScreensaverEntityUnitText;
|
let unitText = '';
|
||||||
iconColor = GetScreenSaverEntityColor(leftScreensaverEntity);
|
if (typeof leftScreensaverEntity.ScreensaverEntityUnitText === 'string') {
|
||||||
} else if (typeof val == 'boolean') {
|
if (existsObject(leftScreensaverEntity.ScreensaverEntityUnitText)) {
|
||||||
iconColor = GetScreenSaverEntityColor(leftScreensaverEntity);
|
unitText = getState(leftScreensaverEntity.ScreensaverEntityUnitText).val;
|
||||||
if (!val && leftScreensaverEntity.ScreensaverEntityIconOff != null) {
|
} else {
|
||||||
icon = Icons.GetIcon(leftScreensaverEntity.ScreensaverEntityIconOff);
|
unitText = leftScreensaverEntity.ScreensaverEntityUnitText;
|
||||||
}
|
}
|
||||||
} else if (typeof val == 'string') {
|
}
|
||||||
iconColor = GetScreenSaverEntityColor(leftScreensaverEntity);
|
|
||||||
let pformat = parseFormat(val);
|
val = val.toFixed(leftScreensaverEntity.ScreensaverEntityDecimalPlaces) + unitText;
|
||||||
if (Debug) log('moments.js --> Datum ' + val + ' valid?: ' + moment(val, pformat, true).isValid(), 'info');
|
iconColor = GetScreenSaverEntityColor(leftScreensaverEntity);
|
||||||
if (moment(val, pformat, true).isValid()) {
|
} else if (typeof val == 'boolean') {
|
||||||
let DatumZeit = moment(val, pformat).unix(); // Umwandlung in Unix Time-Stamp
|
iconColor = GetScreenSaverEntityColor(leftScreensaverEntity);
|
||||||
if (leftScreensaverEntity.ScreensaverEntityDateFormat !== undefined) {
|
if (!val && leftScreensaverEntity.ScreensaverEntityIconOff != null) {
|
||||||
val = new Date(DatumZeit * 1000).toLocaleString(getState(NSPanel_Path + 'Config.locale').val, leftScreensaverEntity.ScreensaverEntityDateFormat);
|
icon = Icons.GetIcon(leftScreensaverEntity.ScreensaverEntityIconOff);
|
||||||
} else {
|
}
|
||||||
val = new Date(DatumZeit * 1000).toLocaleString(getState(NSPanel_Path + 'Config.locale').val);
|
} 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');
|
||||||
const temp = leftScreensaverEntity.ScreensaverEntityIconColor;
|
if (moment(val, pformat, true).isValid()) {
|
||||||
if (temp && typeof temp == 'string' && existsObject(temp)) {
|
let DatumZeit = moment(val, pformat).unix(); // Umwandlung in Unix Time-Stamp
|
||||||
iconColor = getState(temp).val;
|
if (leftScreensaverEntity.ScreensaverEntityDateFormat !== undefined) {
|
||||||
}
|
val = new Date(DatumZeit * 1000).toLocaleString(getState(NSPanel_Path + 'Config.locale').val, leftScreensaverEntity.ScreensaverEntityDateFormat);
|
||||||
|
} else {
|
||||||
payloadString += '~' + '~' + icon + '~' + iconColor + '~' + leftScreensaverEntity.ScreensaverEntityText + '~' + val + '~';
|
val = new Date(DatumZeit * 1000).toLocaleString(getState(NSPanel_Path + 'Config.locale').val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (i < 3) {
|
const temp = leftScreensaverEntity.ScreensaverEntityIconColor;
|
||||||
checkpoint = false;
|
if (temp && typeof temp == 'string' && existsObject(temp)) {
|
||||||
}
|
iconColor = getState(temp).val;
|
||||||
|
}
|
||||||
if (checkpoint == false) {
|
|
||||||
for (let j = i; j < 3; j++) {
|
payloadString += '~' + '~' + icon + '~' + iconColor + '~' + leftScreensaverEntity.ScreensaverEntityText + '~' + val + '~';
|
||||||
payloadString += '~~~~~~';
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
if (i < 3) {
|
||||||
|
checkpoint = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkpoint == false) {
|
||||||
|
for (let j = i; j < 3; j++) {
|
||||||
|
payloadString += '~~~~~~';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 6 bottomScreensaverEntities
|
// 6 bottomScreensaverEntities
|
||||||
let maxEntities: number = 7;
|
let maxEntities: number = 7;
|
||||||
@@ -12594,17 +12692,41 @@ function HandleScreensaverUpdate (): void {
|
|||||||
DayOfWeek = existsObject('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.timestamp')
|
DayOfWeek = existsObject('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.timestamp')
|
||||||
? formatDate(getDateObject((getState('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.timestamp').val)), 'W', 'de')
|
? formatDate(getDateObject((getState('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.timestamp').val)), 'W', 'de')
|
||||||
: 0;
|
: 0;
|
||||||
WeatherIcon = existsObject('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.icon')
|
WeatherIcon = existsObject('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.icon_special')
|
||||||
? GetPirateWeatherIcon(getState('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.icon').val)
|
? getState('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.icon_special').val
|
||||||
: '';
|
: '';
|
||||||
WheatherColor = existsObject('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.icon')
|
WheatherColor = existsObject('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.icon_special')
|
||||||
? GetPirateWeatherIconColor(String(getState('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.icon').val))
|
? GetBrightskyWeatherIconColor(String(getState('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.icon_special').val))
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
RegisterScreensaverEntityWatcher('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.temperature_min');
|
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) + '.temperature_max');
|
||||||
RegisterScreensaverEntityWatcher('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.timestamp');
|
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';
|
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') + '~';
|
payloadString += '~' + '~' + Icons.GetIcon(sun) + '~' + rgb_dec565(MSYellow) + '~' + 'Sonne' + '~' + formatDate(getDateObject(arraySunEvent[nextSunEvent]), 'hh:mm') + '~';
|
||||||
} else {
|
} else {
|
||||||
payloadString += '~' + '~' + Icons.GetIcon(WeatherIcon) + '~' + WheatherColor + '~' + DayOfWeek + '~' + tempMinMaxString + '~';
|
payloadString += '~' + '~' + Icons.GetIcon(WeatherIcon) + '~' + WheatherColor + '~' + DayOfWeek + '~' + tempMinMaxString + '~';
|
||||||
@@ -13686,6 +13832,193 @@ function GetOpenWeatherMapIconColor (icon: string): number {
|
|||||||
return 0;
|
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.
|
* Retrieves the PirateWeather icon string based on the provided icon string.
|
||||||
*
|
*
|
||||||
@@ -13875,6 +14208,82 @@ function GetPirateWeatherIconColor (icon: string): number {
|
|||||||
return 0;
|
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
|
//------------------Begin Read Internal Sensor Data
|
||||||
//mqttCallback (topic: string, message: string): Promise<void> {
|
//mqttCallback (topic: string, message: string): Promise<void> {
|
||||||
/**
|
/**
|
||||||
@@ -14564,6 +14973,7 @@ function isEventMethod (F: string | NSPanel.EventMethod): F is NSPanel.EventMeth
|
|||||||
case 'sleepReached':
|
case 'sleepReached':
|
||||||
case 'pageOpenDetail':
|
case 'pageOpenDetail':
|
||||||
case 'buttonPress2':
|
case 'buttonPress2':
|
||||||
|
case 'buttonPress3':
|
||||||
case 'renderCurrentPage':
|
case 'renderCurrentPage':
|
||||||
case 'button1':
|
case 'button1':
|
||||||
case 'button2':
|
case 'button2':
|
||||||
@@ -14628,7 +15038,7 @@ function isPagePower (F: NSPanel.PageType | NSPanel.PagePower): F is NSPanel.Pag
|
|||||||
namespace NSPanel {
|
namespace NSPanel {
|
||||||
export type PopupType = 'popupFan' | 'popupInSel' | 'popupLight' | 'popupNotify' | 'popupShutter' | 'popupShutter2' | 'popupSlider' | 'popupThermo' | 'popupTimer' | 'popupColor';
|
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 = {
|
export type panelRecvType = {
|
||||||
event: 'event';
|
event: 'event';
|
||||||
method: EventMethod;
|
method: EventMethod;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ repo_name: jobr99/nspanel-lovelace-ui
|
|||||||
repo_url: https://github.com/jobr99/nspanel-lovelace-ui
|
repo_url: https://github.com/jobr99/nspanel-lovelace-ui
|
||||||
edit_uri: ""
|
edit_uri: ""
|
||||||
|
|
||||||
copyright: "Copyright © 2023 Johannes Braun"
|
copyright: "Copyright © 2026 Johannes Braun"
|
||||||
|
|
||||||
docs_dir: docs
|
docs_dir: docs
|
||||||
|
|
||||||
@@ -42,8 +42,6 @@ extra_css:
|
|||||||
extra:
|
extra:
|
||||||
analytics:
|
analytics:
|
||||||
provider: custom
|
provider: custom
|
||||||
version:
|
|
||||||
provider: mike
|
|
||||||
|
|
||||||
markdown_extensions:
|
markdown_extensions:
|
||||||
- admonition
|
- admonition
|
||||||
@@ -63,7 +61,6 @@ markdown_extensions:
|
|||||||
plugins:
|
plugins:
|
||||||
- search:
|
- search:
|
||||||
lang: en
|
lang: en
|
||||||
- mkdocs-video
|
|
||||||
|
|
||||||
nav:
|
nav:
|
||||||
- "Overview": index.md
|
- "Overview": index.md
|
||||||
@@ -75,6 +72,7 @@ nav:
|
|||||||
- "FAQ": faq.md
|
- "FAQ": faq.md
|
||||||
- "Configuration - apps.yaml (Home Assistant)":
|
- "Configuration - apps.yaml (Home Assistant)":
|
||||||
- "Overview": config-overview.md
|
- "Overview": config-overview.md
|
||||||
|
#- "Migration to Standalone Rewrite": config-migration-standalone.md
|
||||||
- "Screensaver": config-screensaver.md
|
- "Screensaver": config-screensaver.md
|
||||||
- "Cards":
|
- "Cards":
|
||||||
- "Entities Card": card-entities.md
|
- "Entities Card": card-entities.md
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# https://developers.home-assistant.io/docs/add-ons/configuration#add-on-config
|
# https://developers.home-assistant.io/docs/add-ons/configuration#add-on-config
|
||||||
name: NSPanel Lovelace UI Addon
|
name: NSPanel Lovelace UI Addon
|
||||||
version: "4.7.80"
|
version: "4.7.85"
|
||||||
slug: nspanel-lovelace-ui
|
slug: nspanel-lovelace-ui
|
||||||
description: NSPanel Lovelace UI Addon
|
description: NSPanel Lovelace UI Addon
|
||||||
services:
|
services:
|
||||||
|
|||||||
@@ -475,7 +475,7 @@ class AlarmCard(HACard):
|
|||||||
main_entity = self.entities[0]
|
main_entity = self.entities[0]
|
||||||
main_entity.render()
|
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")
|
icon = get_icon_char("shield-off")
|
||||||
color = rgb_dec565([255,255,255])
|
color = rgb_dec565([255,255,255])
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ def wait_for_ha_cache():
|
|||||||
while time.time() < mustend:
|
while time.time() < mustend:
|
||||||
if len(libs.home_assistant.home_assistant_entity_state_cache) == 0:
|
if len(libs.home_assistant.home_assistant_entity_state_cache) == 0:
|
||||||
time.sleep(0.1)
|
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)
|
time.sleep(1)
|
||||||
|
|
||||||
def calculate_dim_values(sleepTracking, sleepTrackingZones, sleepBrightness, screenBrightness, sleepOverride, return_involved_entities=False):
|
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)
|
involved_entities.append(sleepBrightness)
|
||||||
try:
|
try:
|
||||||
dimmode = int(float(libs.home_assistant.get_entity_data(sleepBrightness).get('state', 10)))
|
dimmode = int(float(libs.home_assistant.get_entity_data(sleepBrightness).get('state', 10)))
|
||||||
except ValueError:
|
except (TypeError, ValueError):
|
||||||
print("sleepBrightness entity invalid")
|
logging.exception("sleepBrightness entity '%s' has an invalid state value", sleepBrightness)
|
||||||
|
|
||||||
if screenBrightness:
|
if screenBrightness:
|
||||||
if isinstance(screenBrightness, int):
|
if isinstance(screenBrightness, int):
|
||||||
@@ -44,8 +46,8 @@ def calculate_dim_values(sleepTracking, sleepTrackingZones, sleepBrightness, scr
|
|||||||
involved_entities.append(screenBrightness)
|
involved_entities.append(screenBrightness)
|
||||||
try:
|
try:
|
||||||
dimValueNormal = int(float(libs.home_assistant.get_entity_data(screenBrightness).get('state', 100)))
|
dimValueNormal = int(float(libs.home_assistant.get_entity_data(screenBrightness).get('state', 100)))
|
||||||
except ValueError:
|
except (TypeError, ValueError):
|
||||||
print("screenBrightness entity invalid")
|
logging.exception("screenBrightness entity '%s' has an invalid state value", screenBrightness)
|
||||||
# force sleep brightness to zero in case sleepTracking is active
|
# force sleep brightness to zero in case sleepTracking is active
|
||||||
if sleepTracking:
|
if sleepTracking:
|
||||||
if libs.home_assistant.is_existent(sleepTracking):
|
if libs.home_assistant.is_existent(sleepTracking):
|
||||||
@@ -69,12 +71,13 @@ def calculate_dim_values(sleepTracking, sleepTrackingZones, sleepBrightness, scr
|
|||||||
else:
|
else:
|
||||||
return dimmode, dimValueNormal
|
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:
|
match btype:
|
||||||
case 'button':
|
case 'button':
|
||||||
button_press(entity_id, value)
|
button_press(entity_id, value, action_context=action_context)
|
||||||
case 'OnOff':
|
case 'OnOff':
|
||||||
on_off(entity_id, value)
|
on_off(entity_id, value, action_context=action_context)
|
||||||
case 'number-set':
|
case 'number-set':
|
||||||
if entity_id.startswith('fan'):
|
if entity_id.startswith('fan'):
|
||||||
attr = libs.home_assistant.get_entity_data(entity_id).get('attributes', [])
|
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 = {
|
service_data = {
|
||||||
"value": int(value)
|
"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':
|
case 'up' | 'stop' | 'down' | 'tiltOpen' | 'tiltStop' | 'tiltClose' | 'media-next' | 'media-back' | 'media-pause' | 'timer-cancel' | 'timer-pause' | 'timer-finish':
|
||||||
action_service_mapping = {
|
action_service_mapping = {
|
||||||
'up': 'open_cover',
|
'up': 'open_cover',
|
||||||
@@ -99,37 +102,37 @@ def handle_buttons(entity_id, btype, value, entity_config=None):
|
|||||||
'timer-finish': 'finish',
|
'timer-finish': 'finish',
|
||||||
}
|
}
|
||||||
service = action_service_mapping[btype]
|
service = action_service_mapping[btype]
|
||||||
call_ha_service(entity_id, service)
|
call_ha_service(entity_id, service, action_context=action_context)
|
||||||
case 'timer-start':
|
case 'timer-start':
|
||||||
if value:
|
if value:
|
||||||
service_data = {
|
service_data = {
|
||||||
"duration": value
|
"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:
|
else:
|
||||||
call_ha_service(entity_id, "start")
|
call_ha_service(entity_id, "start", action_context=action_context)
|
||||||
case 'positionSlider':
|
case 'positionSlider':
|
||||||
service_data = {
|
service_data = {
|
||||||
"position": int(value)
|
"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':
|
case 'tiltSlider':
|
||||||
service_data = {
|
service_data = {
|
||||||
"tilt_position": int(value)
|
"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':
|
case 'media-OnOff':
|
||||||
state = libs.home_assistant.get_entity_data(entity_id).get('state', '')
|
state = libs.home_assistant.get_entity_data(entity_id).get('state', '')
|
||||||
if state == "off":
|
if state == "off":
|
||||||
call_ha_service(entity_id, "turn_on")
|
call_ha_service(entity_id, "turn_on", action_context=action_context)
|
||||||
else:
|
else:
|
||||||
call_ha_service(entity_id, "turn_off")
|
call_ha_service(entity_id, "turn_off", action_context=action_context)
|
||||||
case 'media-shuffle':
|
case 'media-shuffle':
|
||||||
suffle = libs.home_assistant.get_entity_data(entity_id).get('attributes', []).get('shuffle')
|
suffle = libs.home_assistant.get_entity_data(entity_id).get('attributes', []).get('shuffle')
|
||||||
service_data = {
|
service_data = {
|
||||||
"shuffle": not suffle
|
"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':
|
case 'volumeSlider':
|
||||||
pos = int(value)
|
pos = int(value)
|
||||||
# HA wants to have this value between 0 and 1 as float
|
# 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 = {
|
service_data = {
|
||||||
"volume_level": pos
|
"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':
|
case 'speaker-sel':
|
||||||
service_data = {
|
service_data = {
|
||||||
"volume_level": value
|
"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
|
# for light detail page
|
||||||
case 'brightnessSlider':
|
case 'brightnessSlider':
|
||||||
# scale 0-100 to ha brightness range
|
# scale 0-100 to ha brightness range
|
||||||
@@ -150,7 +153,7 @@ def handle_buttons(entity_id, btype, value, entity_config=None):
|
|||||||
service_data = {
|
service_data = {
|
||||||
"brightness": brightness
|
"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':
|
case 'colorTempSlider':
|
||||||
attr = libs.home_assistant.get_entity_data(entity_id).get('attributes', [])
|
attr = libs.home_assistant.get_entity_data(entity_id).get('attributes', [])
|
||||||
min_mireds = attr.get("min_mireds")
|
min_mireds = attr.get("min_mireds")
|
||||||
@@ -160,19 +163,19 @@ def handle_buttons(entity_id, btype, value, entity_config=None):
|
|||||||
service_data = {
|
service_data = {
|
||||||
"color_temp": color_val
|
"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':
|
case 'colorWheel':
|
||||||
value = value.split('|')
|
value = value.split('|')
|
||||||
color = pos_to_color(int(value[0]), int(value[1]), int(value[2]))
|
color = pos_to_color(int(value[0]), int(value[1]), int(value[2]))
|
||||||
service_data = {
|
service_data = {
|
||||||
"rgb_color": color
|
"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':
|
case 'disarm' | 'arm_home' | 'arm_away' | 'arm_night' | 'arm_vacation':
|
||||||
service_data = {
|
service_data = {
|
||||||
"code": value
|
"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':
|
case 'mode-preset_modes' | 'mode-swing_modes' | 'mode-fan_modes':
|
||||||
attr = libs.home_assistant.get_entity_data(entity_id).get('attributes', [])
|
attr = libs.home_assistant.get_entity_data(entity_id).get('attributes', [])
|
||||||
mapping = {
|
mapping = {
|
||||||
@@ -187,7 +190,7 @@ def handle_buttons(entity_id, btype, value, entity_config=None):
|
|||||||
service_data = {
|
service_data = {
|
||||||
mapping[btype][:-1]: mode
|
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':
|
case 'mode-input_select' | 'mode-select':
|
||||||
options = libs.home_assistant.get_entity_data(entity_id).get('attributes', []).get("options", [])
|
options = libs.home_assistant.get_entity_data(entity_id).get('attributes', []).get("options", [])
|
||||||
if options:
|
if options:
|
||||||
@@ -195,7 +198,7 @@ def handle_buttons(entity_id, btype, value, entity_config=None):
|
|||||||
service_data = {
|
service_data = {
|
||||||
"option": option
|
"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':
|
case 'mode-media_player':
|
||||||
options = libs.home_assistant.get_entity_data(entity_id).get('attributes', []).get("source_list", [])
|
options = libs.home_assistant.get_entity_data(entity_id).get('attributes', []).get("source_list", [])
|
||||||
if options:
|
if options:
|
||||||
@@ -203,7 +206,7 @@ def handle_buttons(entity_id, btype, value, entity_config=None):
|
|||||||
service_data = {
|
service_data = {
|
||||||
"source": option
|
"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':
|
case 'mode-light':
|
||||||
options = entity_config.get("effectList", libs.home_assistant.get_entity_data(entity_id).get('attributes', []).get("effect_list", []))
|
options = entity_config.get("effectList", libs.home_assistant.get_entity_data(entity_id).get('attributes', []).get("effect_list", []))
|
||||||
if options:
|
if options:
|
||||||
@@ -211,13 +214,13 @@ def handle_buttons(entity_id, btype, value, entity_config=None):
|
|||||||
service_data = {
|
service_data = {
|
||||||
"effect": option
|
"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':
|
case 'tempUpd':
|
||||||
temp = int(value)/10
|
temp = int(value)/10
|
||||||
service_data = {
|
service_data = {
|
||||||
"temperature": temp
|
"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':
|
case 'tempUpdHighLow':
|
||||||
value = value.split("|")
|
value = value.split("|")
|
||||||
temp_high = int(value[0])/10
|
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_high": temp_high,
|
||||||
"target_temp_low": temp_low,
|
"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':
|
case 'hvac_action':
|
||||||
service_data = {
|
service_data = {
|
||||||
"hvac_mode": value
|
"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 _:
|
case _:
|
||||||
logging.error("Not implemented: %s", btype)
|
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]
|
etype = entity_id.split(".")[0]
|
||||||
libs.home_assistant.call_service(
|
ok = libs.home_assistant.call_service(
|
||||||
entity_name=entity_id,
|
entity_name=entity_id,
|
||||||
domain=etype,
|
domain=etype,
|
||||||
service=service,
|
service=service,
|
||||||
service_data=service_data
|
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]
|
etype = entity_id.split(".")[0]
|
||||||
match etype:
|
match etype:
|
||||||
case 'scene' | 'script':
|
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':
|
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':
|
case 'lock':
|
||||||
state = libs.home_assistant.get_entity_data(entity_id).get('state', '')
|
state = libs.home_assistant.get_entity_data(entity_id).get('state', '')
|
||||||
if state == "locked":
|
if state == "locked":
|
||||||
call_ha_service(entity_id, "unlock")
|
call_ha_service(entity_id, "unlock", action_context=action_context)
|
||||||
else:
|
else:
|
||||||
call_ha_service(entity_id, "lock")
|
call_ha_service(entity_id, "lock", action_context=action_context)
|
||||||
case 'button' | 'input_button':
|
case 'button' | 'input_button':
|
||||||
call_ha_service(entity_id, "press")
|
call_ha_service(entity_id, "press", action_context=action_context)
|
||||||
case 'input_select' | 'select':
|
case 'input_select' | 'select':
|
||||||
call_ha_service(entity_id, "select_next")
|
call_ha_service(entity_id, "select_next", action_context=action_context)
|
||||||
case 'vacuum':
|
case 'vacuum':
|
||||||
state = libs.home_assistant.get_entity_data(entity_id).get('state', '')
|
state = libs.home_assistant.get_entity_data(entity_id).get('state', '')
|
||||||
if state == "docked":
|
if state == "docked":
|
||||||
call_ha_service(entity_id, "start")
|
call_ha_service(entity_id, "start", action_context=action_context)
|
||||||
else:
|
else:
|
||||||
call_ha_service(entity_id, "return_to_base")
|
call_ha_service(entity_id, "return_to_base", action_context=action_context)
|
||||||
case _:
|
case _:
|
||||||
logging.error("buttonpress for entity type %s not implemented", etype)
|
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(
|
# apis.ha_api.call_service(entity_id.replace(
|
||||||
# 'service.', '', 1).replace('.', '/', 1), **entity_config.data)
|
# '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]
|
etype = entity_id.split(".")[0]
|
||||||
match etype:
|
match etype:
|
||||||
case 'light' | 'switch' | 'input_boolean' | 'automation' | 'fan':
|
case 'light' | 'switch' | 'input_boolean' | 'automation' | 'fan':
|
||||||
service = "turn_off"
|
service = "turn_off"
|
||||||
if value == "1":
|
if value == "1":
|
||||||
service = "turn_on"
|
service = "turn_on"
|
||||||
call_ha_service(entity_id, service)
|
call_ha_service(entity_id, service, action_context=action_context)
|
||||||
case _:
|
case _:
|
||||||
logging.error(
|
logging.error(
|
||||||
"Control action on_off not implemented for %s", entity_id)
|
"Control action on_off not implemented for %s", entity_id)
|
||||||
|
|||||||
@@ -14,8 +14,9 @@ next_id = 0
|
|||||||
request_all_states_id = 0
|
request_all_states_id = 0
|
||||||
ws_connected = False
|
ws_connected = False
|
||||||
home_assistant_entity_state_cache = {}
|
home_assistant_entity_state_cache = {}
|
||||||
template_cache = {}
|
template_cache = {}
|
||||||
response_buffer = {}
|
response_buffer = {}
|
||||||
|
nspanel_event_handler = None
|
||||||
|
|
||||||
|
|
||||||
ON_CONNECT_HANDLER = None
|
ON_CONNECT_HANDLER = None
|
||||||
@@ -44,47 +45,64 @@ def register_on_disconnect_handler(handler):
|
|||||||
ON_DISCONNECT_HANDLER = handler
|
ON_DISCONNECT_HANDLER = handler
|
||||||
|
|
||||||
|
|
||||||
def on_message(ws, message):
|
def on_message(ws, message):
|
||||||
global auth_ok, request_all_states_id, home_assistant_entity_state_cache, response_buffer, template_cache
|
global auth_ok, request_all_states_id, home_assistant_entity_state_cache, response_buffer, template_cache
|
||||||
json_msg = json.loads(message)
|
try:
|
||||||
if json_msg["type"] == "auth_required":
|
json_msg = json.loads(message)
|
||||||
authenticate_client()
|
except json.JSONDecodeError:
|
||||||
elif json_msg["type"] == "auth_ok":
|
logging.exception("Failed to parse Home Assistant websocket message as JSON")
|
||||||
auth_ok = True
|
return
|
||||||
logging.info("Home Assistant auth OK. Requesting existing states.")
|
|
||||||
subscribe_to_events()
|
message_type = json_msg.get("type")
|
||||||
_get_all_states()
|
if message_type == "auth_required":
|
||||||
if ON_CONNECT_HANDLER is not None:
|
authenticate_client()
|
||||||
ON_CONNECT_HANDLER()
|
elif message_type == "auth_ok":
|
||||||
# for templates
|
auth_ok = True
|
||||||
elif json_msg["type"] == "event" and json_msg["id"] in response_buffer:
|
logging.info("Home Assistant auth OK. Requesting existing states.")
|
||||||
template_cache[response_buffer[json_msg["id"]]] = {
|
subscribe_to_events()
|
||||||
"result": json_msg["event"]["result"],
|
_get_all_states()
|
||||||
"listener-entities": json_msg["event"]["listeners"]["entities"]
|
if ON_CONNECT_HANDLER is not None:
|
||||||
}
|
ON_CONNECT_HANDLER()
|
||||||
elif json_msg["type"] == "event" and json_msg["event"]["event_type"] == "state_changed":
|
# for templates
|
||||||
entity_id = json_msg["event"]["data"]["entity_id"]
|
elif message_type == "event" and json_msg.get("id") in response_buffer:
|
||||||
home_assistant_entity_state_cache[entity_id] = json_msg["event"]["data"]["new_state"]
|
event = json_msg.get("event", {})
|
||||||
send_entity_update(entity_id)
|
listeners = event.get("listeners", {})
|
||||||
# rerender template
|
template_cache[response_buffer[json_msg["id"]]] = {
|
||||||
for template, template_cache_entry in template_cache.items():
|
"result": event.get("result"),
|
||||||
if entity_id in template_cache_entry.get("listener-entities", []):
|
"listener-entities": listeners.get("entities", [])
|
||||||
cache_template(template)
|
}
|
||||||
elif json_msg["type"] == "event" and json_msg["event"]["event_type"] == "esphome.nspanel.data":
|
elif message_type == "event" and json_msg.get("event", {}).get("event_type") == "state_changed":
|
||||||
nspanel_data_callback(json_msg["event"]["data"]["device_id"], json_msg["event"]["data"]["CustomRecv"])
|
event_data = json_msg.get("event", {}).get("data", {})
|
||||||
elif json_msg["type"] == "result" and not json_msg["success"]:
|
entity_id = event_data.get("entity_id")
|
||||||
logging.error("Failed result: ")
|
if not entity_id:
|
||||||
logging.error(json_msg)
|
logging.debug("Received state_changed event without entity_id")
|
||||||
elif json_msg["type"] == "result" and json_msg["success"]:
|
return
|
||||||
if json_msg["id"] == request_all_states_id:
|
home_assistant_entity_state_cache[entity_id] = event_data.get("new_state")
|
||||||
for entity in json_msg["result"]:
|
send_entity_update(entity_id)
|
||||||
home_assistant_entity_state_cache[entity["entity_id"]] = entity
|
# rerender template
|
||||||
else:
|
for template, template_cache_entry in template_cache.items():
|
||||||
if json_msg["id"] in response_buffer and json_msg.get("result"):
|
if entity_id in template_cache_entry.get("listener-entities", []):
|
||||||
response_buffer[json_msg["id"]] = json_msg["result"]
|
cache_template(template)
|
||||||
return None # Ignore success result messages
|
elif message_type == "event" and json_msg.get("event", {}).get("event_type") == "esphome.nspanel.data":
|
||||||
else:
|
event_data = json_msg.get("event", {}).get("data", {})
|
||||||
logging.debug(message)
|
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):
|
def _ws_connection_open(ws):
|
||||||
@@ -95,20 +113,24 @@ def _ws_connection_open(ws):
|
|||||||
ON_CONNECT_HANDLER()
|
ON_CONNECT_HANDLER()
|
||||||
|
|
||||||
|
|
||||||
def _ws_connection_close(ws, close_status_code, close_msg):
|
def _ws_connection_close(ws, close_status_code, close_msg):
|
||||||
global ws_connected
|
global ws_connected
|
||||||
ws_connected = False
|
ws_connected = False
|
||||||
logging.error("WebSocket connection closed!")
|
logging.error(
|
||||||
if ON_DISCONNECT_HANDLER is not None:
|
"WebSocket connection closed (status=%s, message=%s)",
|
||||||
ON_DISCONNECT_HANDLER()
|
close_status_code,
|
||||||
|
close_msg,
|
||||||
|
)
|
||||||
|
if ON_DISCONNECT_HANDLER is not None:
|
||||||
|
ON_DISCONNECT_HANDLER()
|
||||||
|
|
||||||
|
|
||||||
def connect():
|
def connect():
|
||||||
Thread(target=_do_connection, daemon=True).start()
|
Thread(target=_do_connection, daemon=True).start()
|
||||||
|
|
||||||
|
|
||||||
def _do_connection():
|
def _do_connection():
|
||||||
global home_assistant_url, ws, settings
|
global home_assistant_url, ws, settings
|
||||||
ws_url = home_assistant_url.replace(
|
ws_url = home_assistant_url.replace(
|
||||||
"https://", "wss://").replace("http://", "ws://")
|
"https://", "wss://").replace("http://", "ws://")
|
||||||
if settings["is_addon"]:
|
if settings["is_addon"]:
|
||||||
@@ -117,12 +139,15 @@ def _do_connection():
|
|||||||
ws_url += "/api/websocket"
|
ws_url += "/api/websocket"
|
||||||
ws = websocket.WebSocketApp(F"{ws_url}", on_message=on_message,
|
ws = websocket.WebSocketApp(F"{ws_url}", on_message=on_message,
|
||||||
on_open=_ws_connection_open, on_close=_ws_connection_close)
|
on_open=_ws_connection_open, on_close=_ws_connection_close)
|
||||||
while True:
|
while True:
|
||||||
logging.info(F"Connecting to Home Assistant at {ws_url}")
|
logging.info(F"Connecting to Home Assistant at {ws_url}")
|
||||||
ws.close()
|
try:
|
||||||
time.sleep(1)
|
ws.close()
|
||||||
ws.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE})
|
time.sleep(1)
|
||||||
time.sleep(10)
|
ws.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE})
|
||||||
|
except Exception:
|
||||||
|
logging.exception("WebSocket connection loop failed")
|
||||||
|
time.sleep(10)
|
||||||
|
|
||||||
|
|
||||||
def authenticate_client():
|
def authenticate_client():
|
||||||
@@ -144,9 +169,9 @@ def subscribe_to_events():
|
|||||||
}
|
}
|
||||||
send_message(json.dumps(msg))
|
send_message(json.dumps(msg))
|
||||||
|
|
||||||
def subscribe_to_nspanel_events(nsp_callback):
|
def subscribe_to_nspanel_events(nsp_callback):
|
||||||
global next_id, nspanel_data_callback
|
global next_id, nspanel_event_handler
|
||||||
nspanel_data_callback = nsp_callback
|
nspanel_event_handler = nsp_callback
|
||||||
msg = {
|
msg = {
|
||||||
"id": next_id,
|
"id": next_id,
|
||||||
"type": "subscribe_events",
|
"type": "subscribe_events",
|
||||||
@@ -168,11 +193,13 @@ def send_entity_update(entity_id):
|
|||||||
global on_ha_update
|
global on_ha_update
|
||||||
on_ha_update(entity_id)
|
on_ha_update(entity_id)
|
||||||
|
|
||||||
def nspanel_data_callback(device_id, msg):
|
def nspanel_data_callback(device_id, msg):
|
||||||
global nspanel_data_callback
|
if nspanel_event_handler is None:
|
||||||
nspanel_data_callback(device_id, msg)
|
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
|
global next_id
|
||||||
try:
|
try:
|
||||||
msg = {
|
msg = {
|
||||||
@@ -187,9 +214,12 @@ def call_service(entity_name: str, domain: str, service: str, service_data: dict
|
|||||||
}
|
}
|
||||||
send_message(json.dumps(msg))
|
send_message(json.dumps(msg))
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logging.exception("Failed to call Home Assisatant service.")
|
logging.exception(
|
||||||
return False
|
"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:
|
def send_msg_to_panel(service: str, service_data: dict) -> bool:
|
||||||
global next_id
|
global next_id
|
||||||
@@ -203,9 +233,9 @@ def send_msg_to_panel(service: str, service_data: dict) -> bool:
|
|||||||
}
|
}
|
||||||
send_message(json.dumps(msg))
|
send_message(json.dumps(msg))
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logging.exception("Failed to call Home Assisatant service.")
|
logging.exception("Failed to call Home Assistant panel service: %s", service)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def execute_script(entity_name: str, domain: str, service: str, service_data: dict) -> str:
|
def execute_script(entity_name: str, domain: str, service: str, service_data: dict) -> str:
|
||||||
global next_id, response_buffer
|
global next_id, response_buffer
|
||||||
@@ -241,13 +271,13 @@ def execute_script(entity_name: str, domain: str, service: str, service_data: di
|
|||||||
else:
|
else:
|
||||||
return response_buffer[call_id]["response"]
|
return response_buffer[call_id]["response"]
|
||||||
raise TimeoutError("Did not recive respose in time to HA script call")
|
raise TimeoutError("Did not recive respose in time to HA script call")
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logging.exception("Failed to call Home Assisatant script.")
|
logging.exception("Failed to call Home Assistant script: %s.%s", domain, service)
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def cache_template(template):
|
def cache_template(template):
|
||||||
if not template:
|
if not template:
|
||||||
raise Exception("Invalid template")
|
raise ValueError("Invalid template")
|
||||||
global next_id, response_buffer
|
global next_id, response_buffer
|
||||||
try:
|
try:
|
||||||
call_id = next_id
|
call_id = next_id
|
||||||
@@ -259,9 +289,9 @@ def cache_template(template):
|
|||||||
}
|
}
|
||||||
send_message(json.dumps(msg))
|
send_message(json.dumps(msg))
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logging.exception("Failed to render template.")
|
logging.exception("Failed to render template.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_template(template):
|
def get_template(template):
|
||||||
global template_cache
|
global template_cache
|
||||||
@@ -299,7 +329,12 @@ def is_existent(entity_id: str):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def send_message(message):
|
def send_message(message):
|
||||||
global ws, next_id
|
global ws, next_id
|
||||||
next_id += 1
|
try:
|
||||||
ws.send(message)
|
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")
|
||||||
|
|||||||
@@ -25,53 +25,76 @@ last_settings_file_mtime = 0
|
|||||||
mqtt_connect_time = 0
|
mqtt_connect_time = 0
|
||||||
has_sent_reload_command = False
|
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):
|
def on_ha_update(entity_id):
|
||||||
global panel_in_queues
|
global panel_in_queues
|
||||||
# send HA updates to all panels
|
# send HA updates to all panels
|
||||||
for queue in panel_in_queues.values():
|
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):
|
def on_ha_panel_event(device_id, msg):
|
||||||
global panel_in_queues
|
global panel_in_queues
|
||||||
|
|
||||||
if device_id in panel_in_queues.keys():
|
if device_id in panel_in_queues.keys():
|
||||||
queue = panel_in_queues[device_id]
|
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():
|
def process_output_to_panel():
|
||||||
while True:
|
while True:
|
||||||
msg = panel_out_queue.get()
|
try:
|
||||||
|
msg = panel_out_queue.get()
|
||||||
#client.publish(msg[0], msg[1])
|
service = msg[0] + "_nspanelui_api_call"
|
||||||
#apis.ha_api.call_service(service="esphome/" + self._api_panel_name + "_nspanelui_api_call", command=2, data=msg)
|
service_data = {
|
||||||
service = msg[0] + "_nspanelui_api_call"
|
"data": msg[1],
|
||||||
service_data = {
|
"command": 2
|
||||||
"data": msg[1],
|
|
||||||
"command":2
|
|
||||||
}
|
}
|
||||||
libs.home_assistant.send_msg_to_panel(
|
libs.home_assistant.send_msg_to_panel(
|
||||||
service = service,
|
service=service,
|
||||||
service_data = service_data
|
service_data=service_data
|
||||||
)
|
)
|
||||||
|
except Exception:
|
||||||
|
logging.exception("Failed to process outgoing panel message")
|
||||||
|
|
||||||
|
|
||||||
def connect():
|
def connect():
|
||||||
global settings, panel_out_queue
|
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:
|
if "mqtt_server" in settings and not "use_ha_api" in settings:
|
||||||
MqttManager(settings, panel_out_queue, panel_in_queues)
|
MqttManager(settings, panel_out_queue, panel_in_queues)
|
||||||
else:
|
else:
|
||||||
logging.info("MQTT values not configured, will not connect.")
|
logging.info("MQTT values not configured, will not connect.")
|
||||||
|
|
||||||
# MQTT Connected, start APIs if configured
|
# 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.init(settings, on_ha_update)
|
||||||
libs.home_assistant.connect()
|
libs.home_assistant.connect()
|
||||||
else:
|
else:
|
||||||
logging.info("Home Assistant values not configured, will not connect.")
|
logging.info("Home Assistant values not configured, will not connect.")
|
||||||
|
return
|
||||||
|
|
||||||
|
wait_seconds = 0
|
||||||
while not libs.home_assistant.ws_connected:
|
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)
|
time.sleep(1)
|
||||||
if settings.get("use_ha_api"):
|
if settings.get("use_ha_api"):
|
||||||
libs.home_assistant.subscribe_to_nspanel_events(on_ha_panel_event)
|
libs.home_assistant.subscribe_to_nspanel_events(on_ha_panel_event)
|
||||||
@@ -97,13 +120,20 @@ def setup_panels():
|
|||||||
panel_thread.start()
|
panel_thread.start()
|
||||||
|
|
||||||
def panel_thread_target(queue_in, name, settings_panel, queue_out):
|
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:
|
while True:
|
||||||
msg = queue_in.get()
|
try:
|
||||||
if msg[0] == "MQTT:":
|
msg = queue_in.get()
|
||||||
panel.customrecv_event_callback(msg[1])
|
if msg[0] == "MQTT:":
|
||||||
elif msg[0] == "HA:":
|
panel.customrecv_event_callback(msg[1])
|
||||||
panel.ha_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():
|
def get_config_file():
|
||||||
CONFIG_FILE = os.getenv('CONFIG_FILE')
|
CONFIG_FILE = os.getenv('CONFIG_FILE')
|
||||||
@@ -117,18 +147,27 @@ def get_config(file):
|
|||||||
try:
|
try:
|
||||||
with open(file, 'r', encoding="utf8") as file:
|
with open(file, 'r', encoding="utf8") as file:
|
||||||
settings = yaml.safe_load(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:
|
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 hasattr(exc, 'problem_mark'):
|
||||||
if exc.context != None:
|
if exc.context != None:
|
||||||
print (' parser says\n' + str(exc.problem_mark) + '\n ' +
|
logging.error(
|
||||||
str(exc.problem) + ' ' + str(exc.context) +
|
"Parser says\n%s\n%s %s\nPlease correct data and retry.",
|
||||||
'\nPlease correct data and retry.')
|
str(exc.problem_mark), str(exc.problem), str(exc.context)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
print (' parser says\n' + str(exc.problem_mark) + '\n ' +
|
logging.error(
|
||||||
str(exc.problem) + '\nPlease correct data and retry.')
|
"Parser says\n%s\n%s\nPlease correct data and retry.",
|
||||||
|
str(exc.problem_mark), str(exc.problem)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
print ("Something went wrong while parsing yaml file")
|
logging.exception("Something went wrong while parsing yaml file")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not settings.get("mqtt_username"):
|
if not settings.get("mqtt_username"):
|
||||||
@@ -172,10 +211,14 @@ def config_watch():
|
|||||||
project_files.append(get_config_file())
|
project_files.append(get_config_file())
|
||||||
handler = ConfigChangeEventHandler(project_files)
|
handler = ConfigChangeEventHandler(project_files)
|
||||||
observer = Observer()
|
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()
|
observer.start()
|
||||||
while True:
|
while True:
|
||||||
time.sleep(1)
|
try:
|
||||||
|
time.sleep(1)
|
||||||
|
except Exception:
|
||||||
|
logging.exception("Config watch loop failed")
|
||||||
|
|
||||||
def signal_handler(signum, frame):
|
def signal_handler(signum, frame):
|
||||||
logging.info(f"Received signal {signum}. Initiating restart...")
|
logging.info(f"Received signal {signum}. Initiating restart...")
|
||||||
@@ -194,4 +237,5 @@ if __name__ == '__main__':
|
|||||||
time.sleep(100)
|
time.sleep(100)
|
||||||
else:
|
else:
|
||||||
while True:
|
while True:
|
||||||
time.sleep(100)
|
|
||||||
|
time.sleep(100)
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ class MqttManager:
|
|||||||
self.client.username_pw_set(
|
self.client.username_pw_set(
|
||||||
settings["mqtt_username"], settings["mqtt_password"])
|
settings["mqtt_username"], settings["mqtt_password"])
|
||||||
# Wait for connection
|
# Wait for connection
|
||||||
connection_return_code = 0
|
|
||||||
mqtt_server = settings["mqtt_server"]
|
mqtt_server = settings["mqtt_server"]
|
||||||
mqtt_port = int(settings["mqtt_port"])
|
mqtt_port = int(settings["mqtt_port"])
|
||||||
logging.info("Connecting to %s:%i as %s",
|
logging.info("Connecting to %s:%i as %s",
|
||||||
@@ -28,9 +27,12 @@ class MqttManager:
|
|||||||
try:
|
try:
|
||||||
self.client.connect(mqtt_server, mqtt_port, 5)
|
self.client.connect(mqtt_server, mqtt_port, 5)
|
||||||
break # Connection call did not raise exception, connection is sucessfull
|
break # Connection call did not raise exception, connection is sucessfull
|
||||||
except: # pylint: disable=bare-except
|
except Exception: # pylint: disable=broad-exception-caught
|
||||||
logging.exception(
|
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.)
|
time.sleep(10.)
|
||||||
self.client.loop_start()
|
self.client.loop_start()
|
||||||
process_thread = threading.Thread(target=self.process_in_queue, args=(self.client, self.msg_in_queue))
|
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()
|
process_thread.start()
|
||||||
|
|
||||||
def on_mqtt_connect(self, client, userdata, flags, rc):
|
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")
|
logging.info("Connected to MQTT Server")
|
||||||
# subscribe to panelRecvTopic of each panel
|
# subscribe to panelRecvTopic of each panel
|
||||||
for settings_panel in self.settings["nspanels"].values():
|
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):
|
def on_mqtt_message(self, client, userdata, msg):
|
||||||
try:
|
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
|
return
|
||||||
if msg.topic in self.msg_out_queue_list.keys():
|
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:
|
if "CustomRecv" in data:
|
||||||
queue = self.msg_out_queue_list[msg.topic]
|
queue = self.msg_out_queue_list[msg.topic]
|
||||||
queue.put(("MQTT:", data["CustomRecv"]))
|
queue.put(("MQTT:", data["CustomRecv"]))
|
||||||
|
else:
|
||||||
|
logging.debug("JSON payload on topic '%s' has no 'CustomRecv' key", msg.topic)
|
||||||
else:
|
else:
|
||||||
logging.debug("Received unhandled message on topic: %s", msg.topic)
|
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
|
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:
|
try:
|
||||||
logging.error(msg.payload.decode('utf-8'))
|
logging.error(msg.payload.decode('utf-8'))
|
||||||
except: # pylint: disable=bare-except
|
except Exception: # pylint: disable=broad-exception-caught
|
||||||
logging.error(
|
logging.error(
|
||||||
"Something went wrong when processing the exception message, couldn't decode payload to utf-8.")
|
"Something went wrong when processing the exception message, couldn't decode payload to utf-8.")
|
||||||
|
|
||||||
def process_in_queue(self, client, msg_in_queue):
|
def process_in_queue(self, client, msg_in_queue):
|
||||||
while True:
|
while True:
|
||||||
msg = msg_in_queue.get()
|
try:
|
||||||
client.publish(msg[0], msg[1])
|
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")
|
||||||
|
|||||||
@@ -103,10 +103,13 @@ class LovelaceUIPanel:
|
|||||||
libs.panel_cmd.page_type(self.msg_out_queue, self.sendTopic, "pageStartup")
|
libs.panel_cmd.page_type(self.msg_out_queue, self.sendTopic, "pageStartup")
|
||||||
|
|
||||||
|
|
||||||
def schedule_thread_target(self):
|
def schedule_thread_target(self):
|
||||||
while True:
|
while True:
|
||||||
self.schedule.exec_jobs()
|
try:
|
||||||
time.sleep(1)
|
self.schedule.exec_jobs()
|
||||||
|
except Exception:
|
||||||
|
logging.exception("Scheduler execution failed for panel '%s'", self.name)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
def update_time(self):
|
def update_time(self):
|
||||||
use_timezone = tz.gettz(self.settings["timeZone"])
|
use_timezone = tz.gettz(self.settings["timeZone"])
|
||||||
@@ -200,12 +203,15 @@ class LovelaceUIPanel:
|
|||||||
return card
|
return card
|
||||||
return list(self.cards.values())[0]
|
return list(self.cards.values())[0]
|
||||||
|
|
||||||
def customrecv_event_callback(self, msg):
|
def customrecv_event_callback(self, msg):
|
||||||
logging.debug("Recv Message from NsPanel (%s): %s", self.name, msg)
|
logging.debug("Recv Message from NsPanel (%s): %s", self.name, msg)
|
||||||
msg = msg.split(",")
|
msg = msg.split(",")
|
||||||
# run action based on received command
|
if len(msg) < 2:
|
||||||
if msg[0] == "event":
|
logging.error("Malformed panel message on '%s': %s", self.name, msg)
|
||||||
if msg[1] == "startup":
|
return
|
||||||
|
# run action based on received command
|
||||||
|
if msg[0] == "event":
|
||||||
|
if msg[1] == "startup":
|
||||||
# TODO: Handle Update Messages
|
# TODO: Handle Update Messages
|
||||||
self.update_date()
|
self.update_date()
|
||||||
self.update_time()
|
self.update_time()
|
||||||
@@ -226,15 +232,24 @@ class LovelaceUIPanel:
|
|||||||
self.render_current_page(switchPages=True)
|
self.render_current_page(switchPages=True)
|
||||||
if msg[1] == "renderCurrentPage":
|
if msg[1] == "renderCurrentPage":
|
||||||
self.render_current_page(requested=True)
|
self.render_current_page(requested=True)
|
||||||
if msg[1] == "buttonPress2":
|
if msg[1] == "buttonPress2":
|
||||||
entity_id = msg[2]
|
if len(msg) < 4:
|
||||||
if entity_id == "":
|
logging.error("Malformed buttonPress2 payload on '%s': %s", self.name, msg)
|
||||||
return
|
return
|
||||||
btype = msg[3]
|
entity_id = msg[2]
|
||||||
value = msg[4] if len(msg) > 4 else None
|
if entity_id == "":
|
||||||
if btype == "bExit":
|
return
|
||||||
if entity_id in ["screensaver", "screensaver2"] and self.settings.get("screensaver").get("doubleTapToUnlock") and value == "1":
|
btype = msg[3]
|
||||||
return
|
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
|
# in case privious_cards is empty add a default card
|
||||||
if len(self.privious_cards) == 0:
|
if len(self.privious_cards) == 0:
|
||||||
@@ -249,12 +264,12 @@ class LovelaceUIPanel:
|
|||||||
return
|
return
|
||||||
|
|
||||||
# replace iid with real entity id
|
# replace iid with real entity id
|
||||||
if entity_id.startswith("iid."):
|
if entity_id.startswith("iid."):
|
||||||
iid = entity_id.split(".")[1]
|
iid = entity_id.split(".")[1]
|
||||||
for e in self.current_card.entities:
|
for e in self.current_card.entities:
|
||||||
if e.iid == iid:
|
if e.iid == iid:
|
||||||
entity_id = e.entity_id
|
entity_id = e.entity_id
|
||||||
entity_config = e.config
|
entity_config = e.config
|
||||||
|
|
||||||
match btype:
|
match btype:
|
||||||
case 'button':
|
case 'button':
|
||||||
@@ -271,31 +286,51 @@ class LovelaceUIPanel:
|
|||||||
self.privious_cards.append(self.current_card)
|
self.privious_cards.append(self.current_card)
|
||||||
self.current_card = self.searchCard(card_iid)
|
self.current_card = self.searchCard(card_iid)
|
||||||
self.render_current_page(switchPages=True)
|
self.render_current_page(switchPages=True)
|
||||||
# send ha stuff to ha
|
# send ha stuff to ha
|
||||||
case _:
|
case _:
|
||||||
ha_control.handle_buttons(entity_id, btype, value, entity_config=entity_config)
|
ha_control.handle_buttons(
|
||||||
|
entity_id,
|
||||||
|
btype,
|
||||||
|
value,
|
||||||
|
entity_config=entity_config,
|
||||||
|
action_context=action_context,
|
||||||
|
)
|
||||||
case 'cardUnlock-unlock':
|
case 'cardUnlock-unlock':
|
||||||
card_iid = entity_id.split(".")[1]
|
card_iid = entity_id.split(".")[1]
|
||||||
if int(self.current_card.config.get("pin")) == int(value):
|
if int(self.current_card.config.get("pin")) == int(value):
|
||||||
self.privious_cards.append(self.current_card)
|
self.privious_cards.append(self.current_card)
|
||||||
self.current_card = self.searchCard(card_iid)
|
self.current_card = self.searchCard(card_iid)
|
||||||
self.render_current_page(switchPages=True)
|
self.render_current_page(switchPages=True)
|
||||||
case 'mode-light':
|
case 'mode-light':
|
||||||
ha_control.handle_buttons(entity_id, btype, value, entity_config=entity_config)
|
ha_control.handle_buttons(
|
||||||
case _:
|
entity_id,
|
||||||
ha_control.handle_buttons(entity_id, btype, value)
|
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":
|
if msg[1] == "pageOpenDetail":
|
||||||
entity_id = msg[3]
|
if len(msg) < 4:
|
||||||
# replace iid with real entity id
|
logging.error("Malformed pageOpenDetail payload on '%s': %s", self.name, msg)
|
||||||
if entity_id.startswith("iid."):
|
return
|
||||||
iid = entity_id.split(".")[1]
|
entity_id = msg[3]
|
||||||
for e in self.current_card.entities:
|
effectList = None
|
||||||
if e.iid == iid:
|
# replace iid with real entity id
|
||||||
entity_id = e.entity_id
|
if entity_id.startswith("iid."):
|
||||||
effectList = None
|
iid = entity_id.split(".")[1]
|
||||||
if entity_id.startswith("light"):
|
for e in self.current_card.entities:
|
||||||
effectList = e.config.get("effectList")
|
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']:
|
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))
|
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:
|
else:
|
||||||
|
|||||||
Reference in New Issue
Block a user