mirror of
https://github.com/joBr99/nspanel-lovelace-ui.git
synced 2026-02-22 05:58:39 +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 }}
|
||||
steps:
|
||||
- name: Check out the repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Get changed files
|
||||
id: changed_files
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Get information
|
||||
id: info
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: env.BUILD_ARGS != '--test'
|
||||
uses: docker/login-action@v3.6.0
|
||||
uses: docker/login-action@v3.7.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
|
||||
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
@@ -43,11 +43,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
uses: github/codeql-action/autobuild@v4
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -72,4 +72,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
uses: github/codeql-action/analyze@v4
|
||||
|
||||
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:
|
||||
- main
|
||||
paths:
|
||||
- docs/*
|
||||
- docs/**
|
||||
- docs-standalone/**
|
||||
- .github/workflows/docs-release.yml
|
||||
- mkdocs.yml
|
||||
- docs-standalone/mkdocs.yml
|
||||
- HMI/README.md
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.x
|
||||
- run: pip install mkdocs-material mkdocs-video markdown-include mike
|
||||
- run: pip install zensical
|
||||
- run: cp HMI/README.md docs/hmi-serial-protocol.md
|
||||
- run: git config --global user.name Docs deploy
|
||||
- run: git config --global user.email docs@dummy.bot.com
|
||||
- run: mike set-default stable
|
||||
- run: mike deploy --push --update-aliases stable
|
||||
- run: zensical build --config-file mkdocs.yml
|
||||
- run: mv site _site_main
|
||||
- run: zensical build --config-file docs-standalone/mkdocs.yml
|
||||
- run: mkdir -p _site/standalone _site/stable
|
||||
- run: cp -a _site_main/. _site/
|
||||
- run: cp -a _site_main/. _site/stable/
|
||||
- run: |
|
||||
if [ -d site-standalone ]; then
|
||||
cp -a site-standalone/. _site/standalone/
|
||||
elif [ -d docs-standalone/site ]; then
|
||||
cp -a docs-standalone/site/. _site/standalone/
|
||||
elif [ -d site ]; then
|
||||
cp -a site/. _site/standalone/
|
||||
elif [ -d docs-standalone/build ]; then
|
||||
cp -a docs-standalone/build/. _site/standalone/
|
||||
elif [ -d build ]; then
|
||||
cp -a build/. _site/standalone/
|
||||
else
|
||||
echo "Standalone docs output not found (tried site-standalone, docs-standalone/site, site, docs-standalone/build, build)."
|
||||
exit 1
|
||||
fi
|
||||
- uses: peaceiris/actions-gh-pages@v4
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_branch: gh-pages
|
||||
publish_dir: _site
|
||||
force_orphan: true
|
||||
|
||||
2
.github/workflows/hacs-validation.yaml
vendored
2
.github/workflows/hacs-validation.yaml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
name: HACS Action
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- uses: "actions/checkout@v5"
|
||||
- uses: "actions/checkout@v6"
|
||||
- name: HACS Action
|
||||
uses: "hacs/action@main"
|
||||
with:
|
||||
|
||||
2
.github/workflows/iobroker-localization.yml
vendored
2
.github/workflows/iobroker-localization.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
gen-ioBroker-localization:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
|
||||
|
||||
6
.github/workflows/lint.yaml
vendored
6
.github/workflows/lint.yaml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
addons: ${{ steps.addons.outputs.addons_list }}
|
||||
steps:
|
||||
- name: ⤵️ Check out code from GitHub
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: 🔍 Find add-on directories
|
||||
id: addons
|
||||
@@ -33,9 +33,9 @@ jobs:
|
||||
path: ${{ fromJson(needs.find.outputs.addons) }}
|
||||
steps:
|
||||
- name: ⤵️ Check out code from GitHub
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: 🚀 Run Home Assistant Add-on Lint
|
||||
uses: frenck/action-addon-linter@v2.18
|
||||
uses: frenck/action-addon-linter@v2.21
|
||||
with:
|
||||
path: "./${{ matrix.path }}"
|
||||
|
||||
2
.github/workflows/nextion2text.yml
vendored
2
.github/workflows/nextion2text.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
|
||||
|
||||
29
.gitignore
vendored
29
.gitignore
vendored
@@ -6,4 +6,31 @@ HMI/Nextion2Text.py
|
||||
panels.yaml
|
||||
|
||||
# don't add Webstorm project stuff
|
||||
.idea
|
||||
.idea
|
||||
|
||||
# General
|
||||
.DS_Store
|
||||
__MACOSX/
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
Icon[
|
||||
]
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
@@ -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/joBr99/nspanel-lovelace-ui/releases)
|
||||

|
||||

|
||||
[](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)
|
||||
|
||||
def button_press(self, entity_id, button_type, value):
|
||||
apis.ha_api.log(f"Button Press Event; entity_id: {entity_id}; button_type: {button_type}; value: {value} ")
|
||||
#apis.ha_api.log(f"Button Press Event; entity_id: {entity_id}; button_type: {button_type}; value: {value} ")
|
||||
entity_id_new = entity_id
|
||||
if entity_id.startswith('uuid'):
|
||||
entity_config = self._config._config_entites_table.get(entity_id)
|
||||
if entity_config is not None:
|
||||
entity_id = entity_config.entityId
|
||||
entity_id_new = entity_config.entityId
|
||||
apis.ha_api.log(f"Button Press Event; entity_id: {entity_id}; entity_id_resolved: {entity_id_new}; button_type: {button_type}; value: {value} ")
|
||||
entity_id = entity_id_new
|
||||
|
||||
# internal buttons
|
||||
if entity_id == "screensaver" and button_type == "bExit":
|
||||
# get default card if there is one
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/*.md-header,*/ .md-footer {
|
||||
/*.md-header,*/ .md-footer,
|
||||
.md-footer-meta {
|
||||
background-color: #333333;
|
||||
}
|
||||
|
||||
.md-footer__inner.md-grid {
|
||||
display: none;
|
||||
/* Footer contrast fixes for Zensical/Material variants */
|
||||
:root {
|
||||
--md-footer-bg-color: #333333;
|
||||
--md-footer-bg-color--dark: #2b2b2b;
|
||||
--md-footer-fg-color: #f2f2f2;
|
||||
--md-footer-fg-color--light: #ffffff;
|
||||
--md-footer-fg-color--lighter: #ffffff;
|
||||
}
|
||||
|
||||
.md-footer,
|
||||
.md-footer-meta,
|
||||
.md-footer * {
|
||||
color: #f2f2f2;
|
||||
}
|
||||
|
||||
.md-footer a,
|
||||
.md-footer-meta a,
|
||||
.md-footer .md-footer__link {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.md-footer a:hover,
|
||||
.md-footer-meta a:hover,
|
||||
.md-footer .md-footer__link:hover {
|
||||
color: #d9e7ff;
|
||||
}
|
||||
|
||||
.md-footer .md-icon svg,
|
||||
.md-footer-meta .md-icon svg {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
/* Zensical keeps footer content in the inner/meta containers.
|
||||
Don't hide footer structure, only style it. */
|
||||
|
||||
.md-sidebar {
|
||||
padding-top: 0px;
|
||||
}
|
||||
@@ -77,4 +109,4 @@
|
||||
|
||||
ol li::marker {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
- abgestimmt auf TFT 61 / v5.1.1 / BerryDriver 10 / Tasmota 15.2.0
|
||||
TypeScript v5.1.1.4 zur Steuerung des SONOFF NSPanel mit dem ioBroker by @Armilar / @TT-Tom / @ticaki / @Britzelpuf / @Sternmiere / @ravenS0ne
|
||||
- abgestimmt auf TFT 61 / v5.1.1 (v5.1.2 us-p) / BerryDriver 10 / Tasmota 15.2.0
|
||||
|
||||
Projekt:
|
||||
https://github.com/joBr99/nspanel-lovelace-ui/tree/main/ioBroker
|
||||
@@ -101,7 +101,10 @@ ReleaseNotes:
|
||||
- 21.11.2025 - v5.1.1.1 Add some LongPress Actions in TFT/HMI v5.1.1 for NSPanel Adapter
|
||||
- 21.11.2025 - v5.1.1.1 Remove Subscription if .ON and ON_ACTUAL
|
||||
- 21.12.2025 - v5.1.1.2 Left screensaver unit from ioBroker data point to create a dynamic screensaver (by ernstdaheim-hub)
|
||||
- 29.12.2025 - v5.1.1.3 Fix popupSlider (Standard-Slider (not cardMedia) with Functionality on popupSlider) / Wrong Pictures in us-p Slider if BG-Color is black (0)
|
||||
- 29.12.2025 - v5.1.1.4 Refactor power subscription handling in NSPanelTs (#1421 by lubepi)
|
||||
|
||||
|
||||
***************************************************************************************************************
|
||||
* DE: Für die Erstellung der Aliase durch das Skript, muss in der JavaScript Instanz "setObject" gesetzt sein! *
|
||||
* EN: In order for the script to create the aliases, “setObject” must be set in the JavaScript instance! *
|
||||
@@ -216,7 +219,7 @@ Install/Upgrades in Konsole:
|
||||
TFT EU STABLE Version: FlashNextionAdv0 http://nspanel.de/nspanel-v5.1.1.tft
|
||||
|
||||
TFT US-L STABLE Version: FlashNextionAdv0 http://nspanel.de/nspanel-us-l-v5.1.1.tft
|
||||
TFT US-P STABLE Version: FlashNextionAdv0 http://nspanel.de/nspanel-us-p-v5.1.1.tft
|
||||
TFT US-P STABLE Version: FlashNextionAdv0 http://nspanel.de/nspanel-us-p-v5.1.2.tft
|
||||
---------------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
@@ -1002,7 +1005,7 @@ export const config: Config = {
|
||||
// _________________________________ DE: Ab hier keine Konfiguration mehr _____________________________________
|
||||
// _________________________________ EN: No more configuration from here _____________________________________
|
||||
|
||||
const scriptVersion: string = 'v5.1.1.2';
|
||||
const scriptVersion: string = 'v5.1.1.4';
|
||||
const tft_version: string = 'v5.1.1';
|
||||
const desired_display_firmware_version = 61;
|
||||
const berry_driver_version = 10;
|
||||
@@ -8849,7 +8852,11 @@ function unsubscribePowerSubscriptions (): void {
|
||||
* @returns {void}
|
||||
*/
|
||||
function subscribePowerSubscriptions (id: string): void {
|
||||
on({id: id + '.ACTUAL', change: 'ne'}, async function () {
|
||||
const subscriptionKey = 'power_' + id + '.ACTUAL';
|
||||
if (subscriptions.hasOwnProperty(subscriptionKey)) {
|
||||
return;
|
||||
}
|
||||
subscriptions[subscriptionKey] = on({id: id + '.ACTUAL', change: 'ne'}, async function () {
|
||||
(function () {
|
||||
if (timeoutPower) {
|
||||
clearTimeout(timeoutPower);
|
||||
@@ -8862,7 +8869,6 @@ function subscribePowerSubscriptions (id: string): void {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @function GeneratePowerPage
|
||||
* @description Generates a page with power state and energy usage information.
|
||||
@@ -10188,6 +10194,10 @@ function HandleButtonEvent (words: any): void {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let pageItemSlider = findPageItem(id);
|
||||
let sliderPos = Math.trunc(scale(parseInt(words[4]), 0, 100, pageItemSlider.maxValueLevel ? pageItemSlider.maxValue : 100, pageItemSlider.minValueLevel ? pageItemSlider.minValue : 0));
|
||||
setIfExists(pageItemSlider.id + '.SET', sliderPos) ? true : setIfExists(id + '.ACTUAL', sliderPos);
|
||||
}
|
||||
break;
|
||||
case 'mode-seek':
|
||||
@@ -11448,7 +11458,7 @@ function GenerateDetailPage (type: NSPanel.PopupType, optional: NSPanel.mediaOpt
|
||||
let hSlider2MinVal: number = pageItem.minValue ?? 0;
|
||||
let hSlider2MaxVal: number = pageItem.maxValue ?? 100;
|
||||
let hSlider2ZeroVal: number = 0;
|
||||
let hSlider2CurVal: number = getState(id + '.ACTUAL').val;
|
||||
let hSlider2CurVal: number = existsState(id + '.ACTUAL') ? getState(id + '.ACTUAL').val : getState(id + '.SET').val;
|
||||
let hSlider2Step: number = 1;
|
||||
let hSlider2Visibility: string = "enable";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*-----------------------------------------------------------------------
|
||||
TypeScript v5.0.2.1 zur Steuerung des SONOFF NSPanel mit dem ioBroker by @Armilar / @TT-Tom / @ticaki / @Britzelpuf / @Sternmiere / @ravenS0ne
|
||||
- abgestimmt auf TFT 59 / v5.0.2 / BerryDriver 10 / Tasmota 15.0.1
|
||||
TypeScript v5.1.1.4 zur Steuerung des SONOFF NSPanel mit dem ioBroker by @Armilar / @TT-Tom / @ticaki / @Britzelpuf / @Sternmiere / @ravenS0ne
|
||||
- abgestimmt auf TFT 61 / v5.1.1 (v5.1.2 us-p) / BerryDriver 10 / Tasmota 15.2.0
|
||||
|
||||
Projekt:
|
||||
https://github.com/joBr99/nspanel-lovelace-ui/tree/main/ioBroker
|
||||
@@ -94,8 +94,17 @@ ReleaseNotes:
|
||||
- 08.09.2025 - v5.0.0 TFT 59 / 5.0.0 - US-L/US-P Changes in cardMedia, popupInSel, card Grid 1, 2, 3
|
||||
- 19.09.2025 - v5.0.0.2 Remove Startup Scheedule at 3:30am / Small fix
|
||||
- 19.10.2025 - v5.0.2.1 TFT 59 / 5.0.2 - EU/US-L/US-P - Fix cardAlarm Icon; Fix Notification in Advanced Screensaver; Fix Dimensions in cardChart/cardLChart
|
||||
- 12.11.2025 - v5.1.0 TFT 61 / 5.1.0 - Breaking Changes in popupNotify TFT - add 3. Button only for Adapter
|
||||
- 12.11.2025 - v5.1.0.1 Change Brightsky icon to icon_special
|
||||
- 15.11.2025 - v5.1.0.2 Add Swiss-Weather-API Adapter
|
||||
- 18.11.2025 - v5.1.0.3 Fix QR-Code Generation cardQR
|
||||
- 21.11.2025 - v5.1.1.1 Add some LongPress Actions in TFT/HMI v5.1.1 for NSPanel Adapter
|
||||
- 21.11.2025 - v5.1.1.1 Remove Subscription if .ON and ON_ACTUAL
|
||||
- 21.12.2025 - v5.1.1.2 Left screensaver unit from ioBroker data point to create a dynamic screensaver (by ernstdaheim-hub)
|
||||
- 29.12.2025 - v5.1.1.3 Fix popupSlider (Standard-Slider (not cardMedia) with Functionality on popupSlider) / Wrong Pictures in us-p Slider if BG-Color is black (0)
|
||||
- 29.12.2025 - v5.1.1.4 Refactor power subscription handling in NSPanelTs (#1421 by lubepi)
|
||||
|
||||
|
||||
|
||||
***************************************************************************************************************
|
||||
* DE: Für die Erstellung der Aliase durch das Skript, muss in der JavaScript Instanz "setObject" gesetzt sein! *
|
||||
* EN: In order for the script to create the aliases, “setObject” must be set in the JavaScript instance! *
|
||||
@@ -190,9 +199,13 @@ Tasmota-Status0 - (zyklische Ausführung)
|
||||
|
||||
Erforderliche Adapter:
|
||||
|
||||
Pirate-Weather oder BrightSky oder OpenWeatherMap --> Bei Nutzung der Wetterfunktionen (und zur Icon-Konvertierung) im Screensaver
|
||||
!!!DasWetter deprecated - Dienst nur noch für ältere Accounts funktional
|
||||
!!!AccuWeather deprecated - Dienst schaltet Free-Account ab!!!
|
||||
Bei Nutzung der Wetterfunktionen (und zur Icon-Konvertierung) im Screensaver einen der folgenden Wetter-Adapter:
|
||||
- Pirate-Weather
|
||||
- BrightSky
|
||||
- OpenWeatherMap
|
||||
- Swiss-Weather-API
|
||||
- !!!DasWetter deprecated - Dienst nur noch für ältere Accounts funktional
|
||||
- !!!AccuWeather deprecated - Dienst schaltet Free-Account ab!!!
|
||||
Alexa2: - Bei Nutzung der dynamischen SpeakerList in der cardMedia
|
||||
Geräte verwalten - Für Erstellung der Aliase
|
||||
MQTT-Adapter - Für Kommunikation zwischen Skript und Tasmota
|
||||
@@ -203,10 +216,10 @@ Install/Upgrades in Konsole:
|
||||
Tasmota BerryDriver Install: Backlog UrlFetch https://raw.githubusercontent.com/ticaki/ioBroker.nspanel-lovelace-ui/refs/heads/main/tasmota/berry/10/autoexec.be; Restart 1
|
||||
Tasmota BerryDriver Update: Backlog UpdateDriverVersion https://raw.githubusercontent.com/ticaki/ioBroker.nspanel-lovelace-ui/refs/heads/main/tasmota/berry/10/autoexec.be; Restart 1
|
||||
|
||||
TFT EU STABLE Version: FlashNextionAdv0 http://nspanel.de/nspanel-v5.0.2.tft
|
||||
TFT EU STABLE Version: FlashNextionAdv0 http://nspanel.de/nspanel-v5.1.1.tft
|
||||
|
||||
TFT US-L STABLE Version: FlashNextionAdv0 http://nspanel.de/nspanel-us-l-v5.0.2.tft
|
||||
TFT US-P STABLE Version: FlashNextionAdv0 http://nspanel.de/nspanel-us-p-v5.0.2.tft
|
||||
TFT US-L STABLE Version: FlashNextionAdv0 http://nspanel.de/nspanel-us-l-v5.1.1.tft
|
||||
TFT US-P STABLE Version: FlashNextionAdv0 http://nspanel.de/nspanel-us-p-v5.1.2.tft
|
||||
---------------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
@@ -254,8 +267,8 @@ const NSPanel_Alarm_Path = '0_userdata.0.NSPanel.';
|
||||
|
||||
/***** 3. Weather adapter Config *****/
|
||||
|
||||
// DE: Mögliche Wetteradapter 'pirate-weather.0.' oder 'brightsky.0.' oder 'openweathermap.0.' oder 'daswetter.0.' (deprecated) oder 'accuweather.0.' (deprecated)
|
||||
// EN: Possible weather adapters 'pirate-weather.0.' or 'brightsky.0.' or 'openweathermap.0.' or 'daswetter.0.' (deprecated) or 'accuweather.0.' (deprecated)
|
||||
// DE: Mögliche Wetteradapter 'pirate-weather.0.' oder 'brightsky.0.' oder 'openweathermap.0.' oder 'swiss-weather-api.0.' oder 'daswetter.0.' (deprecated) oder 'accuweather.0.' (deprecated)
|
||||
// EN: Possible weather adapters 'pirate-weather.0.' or 'brightsky.0.' or 'openweathermap.0.' or 'swiss-weather-api.0.' or 'daswetter.0.' (deprecated) or 'accuweather.0.' (deprecated)
|
||||
const weatherAdapterInstance: string = 'pirate-weather.0.';
|
||||
|
||||
// DE: Mögliche Werte: 'Min', 'Max' oder 'MinMax' im Screensaver
|
||||
@@ -992,9 +1005,9 @@ export const config: Config = {
|
||||
// _________________________________ DE: Ab hier keine Konfiguration mehr _____________________________________
|
||||
// _________________________________ EN: No more configuration from here _____________________________________
|
||||
|
||||
const scriptVersion: string = 'v5.0.2.1';
|
||||
const tft_version: string = 'v5.0.2';
|
||||
const desired_display_firmware_version = 59;
|
||||
const scriptVersion: string = 'v5.1.1.4';
|
||||
const tft_version: string = 'v5.1.1';
|
||||
const desired_display_firmware_version = 61;
|
||||
const berry_driver_version = 10;
|
||||
|
||||
const tasmotaOtaUrl: string = 'http://ota.tasmota.com/tasmota32/release/';
|
||||
@@ -1111,6 +1124,11 @@ async function CheckConfigParameters () {
|
||||
log('Weather adapter: << weatherAdapterInstance - ' + weatherAdapterInstance + ' >> is not installed. Please Check Adapter!', 'error');
|
||||
}
|
||||
}
|
||||
if (weatherAdapterInstance.substring(0, weatherAdapterInstance.length - 3) == 'swiss-weather-api') {
|
||||
if (existsObject(weatherAdapterInstance + 'forecast.days.day0.0000.symbol_code') == false) {
|
||||
log('Weather adapter: << weatherAdapterInstance - ' + weatherAdapterInstance + ' >> is not installed. Please Check Adapter!', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
let weatherAdapterInstanceArray: any = weatherAdapterInstance.split('.');
|
||||
weatherAdapterInstanceNumber = weatherAdapterInstanceArray[1];
|
||||
@@ -2572,11 +2590,11 @@ async function CreateWeatherAlias () {
|
||||
if (!existsState(config.weatherEntity + '.ICON') && existsState('brightsky.' + weatherAdapterInstanceNumber + '.current.icon')) {
|
||||
log('Weather alias for brightsky.' + weatherAdapterInstanceNumber + '. does not exist yet, will be created now', 'info');
|
||||
setObject(config.weatherEntity, {_id: config.weatherEntity, type: 'channel', common: {role: 'weatherCurrent', name: 'weatherCurrent'}, native: {}});
|
||||
await createAliasAsync(config.weatherEntity + '.ICON', ('brightsky.' + weatherAdapterInstanceNumber + '.current.icon'), true, {
|
||||
await createAliasAsync(config.weatherEntity + '.ICON', ('brightsky.' + weatherAdapterInstanceNumber + '.current.icon_special'), true, {
|
||||
type: 'string',
|
||||
role: 'value',
|
||||
name: 'ICON',
|
||||
alias: {id: 'brightsky.' + weatherAdapterInstanceNumber + '.current.icon'},
|
||||
alias: {id: 'brightsky.' + weatherAdapterInstanceNumber + '.current.icon_special'},
|
||||
});
|
||||
await createAliasAsync(config.weatherEntity + '.TEMP', 'brightsky.' + weatherAdapterInstanceNumber + '.current.temperature', true, {
|
||||
type: 'number',
|
||||
@@ -2603,6 +2621,57 @@ async function CreateWeatherAlias () {
|
||||
} catch (err: any) {
|
||||
log('error at function CreateWeatherAlias brightsky.' + weatherAdapterInstanceNumber + '.: ' + err.message, 'warn');
|
||||
}
|
||||
} else if (weatherAdapterInstance == 'swiss-weather-api.' + weatherAdapterInstanceNumber + '.') {
|
||||
try {
|
||||
if (isSetOptionActive) {
|
||||
if (!existsState(config.weatherEntity + '.ICON') && existsState('swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day0.0000.symbol_code')) {
|
||||
log('Weather alias for swiss-weather-api.' + weatherAdapterInstanceNumber + '. does not exist yet, will be created now', 'info');
|
||||
setObject(config.weatherEntity, {
|
||||
_id: config.weatherEntity,
|
||||
type: 'channel',
|
||||
common: {role: 'weatherCurrent', name: 'weatherCurrent'},
|
||||
native: {}
|
||||
});
|
||||
await createAliasAsync(config.weatherEntity + '.ICON', ('swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day0.0000.symbol_code'), true, {
|
||||
type: 'string',
|
||||
role: 'value',
|
||||
name: 'ICON',
|
||||
alias: {id: 'swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day0.0000.symbol_code'},
|
||||
});
|
||||
await createAliasAsync(config.weatherEntity + '.TEMP', 'swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.current_hour.TTT_C', true, {
|
||||
type: 'number',
|
||||
role: 'value.temperature',
|
||||
name: 'TEMP',
|
||||
alias: {
|
||||
id: 'swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.current_hour.TTT_C',
|
||||
read: 'Math.round(val*10)/10'
|
||||
},
|
||||
});
|
||||
await createAliasAsync(config.weatherEntity + '.TEMP_MIN', 'swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day0.0000.TN_C', true, {
|
||||
type: 'number',
|
||||
role: 'value.temperature.forecast.0',
|
||||
name: 'TEMP_MIN',
|
||||
alias: {
|
||||
id: 'swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day0.0000.TN_C',
|
||||
read: 'Math.round(val)'
|
||||
},
|
||||
});
|
||||
await createAliasAsync(config.weatherEntity + '.TEMP_MAX', 'swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day0.0000.TX_C', true, {
|
||||
type: 'number',
|
||||
role: 'value.temperature.max.forecast.0',
|
||||
name: 'TEMP_MAX',
|
||||
alias: {
|
||||
id: 'swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day0.0000.TX_C',
|
||||
read: 'Math.round(val)'
|
||||
},
|
||||
});
|
||||
} else {
|
||||
log('weather alias for swiss-weather-api.' + weatherAdapterInstanceNumber + '. already exists', 'info');
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
log('error at function CreateWeatherAlias swiss-weather-api.' + weatherAdapterInstanceNumber + '.: ' + err.message, 'warn');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
@@ -3218,6 +3287,8 @@ async function InitPopupNotify () {
|
||||
'~' +
|
||||
v_popupNotifyButton1TextColor +
|
||||
'~' +
|
||||
'~' + // Fix Button3_Text for Adapter
|
||||
'~' + // Fix Button3_Color for Adapter
|
||||
getState(popupNotifyButton2Text).val +
|
||||
'~' +
|
||||
v_popupNotifyButton2TextColor +
|
||||
@@ -4664,6 +4735,7 @@ function HandleMessage (typ: string, method: NSPanel.EventMethod, page: number |
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'buttonPress3':
|
||||
case 'buttonPress2':
|
||||
screensaverEnabled = false;
|
||||
HandleButtonEvent(words);
|
||||
@@ -5170,7 +5242,7 @@ function CreateEntity (pageItem: PageItem, placeId: number, useColors: boolean =
|
||||
val = getState(pageItem.id + '.ACTUAL').val;
|
||||
RegisterEntityWatcher(pageItem.id + '.ACTUAL');
|
||||
}
|
||||
if (existsState(pageItem.id + '.SET')) {
|
||||
if (existsState(pageItem.id + '.SET') && !existsState(pageItem.id + 'ACTUAL')) {
|
||||
val = getState(pageItem.id + '.SET').val;
|
||||
RegisterEntityWatcher(pageItem.id + '.SET');
|
||||
}
|
||||
@@ -5183,7 +5255,7 @@ function CreateEntity (pageItem: PageItem, placeId: number, useColors: boolean =
|
||||
val = getState(pageItem.id + '.ON_SET').val;
|
||||
RegisterEntityWatcher(pageItem.id + '.ON_SET');
|
||||
}
|
||||
if (existsState(pageItem.id + '.ON')) {
|
||||
if (existsState(pageItem.id + '.ON') && !existsState(pageItem.id + '.ON_ACTUAL')) {
|
||||
val = getState(pageItem.id + '.ON').val;
|
||||
RegisterEntityWatcher(pageItem.id + '.ON');
|
||||
}
|
||||
@@ -6350,7 +6422,7 @@ function RegisterEntityWatcher (id: string): void {
|
||||
return;
|
||||
}
|
||||
|
||||
subscriptions[id] = on({id: id, change: 'any'}, (obj) => {
|
||||
subscriptions[id] = on({id: id, change: 'ne'}, (obj) => {
|
||||
//@ts-ignore
|
||||
if (obj.oldState && obj.oldState.val === obj.state.val && obj.state.ack) {
|
||||
return;
|
||||
@@ -8597,7 +8669,7 @@ async function createAutoQRAlias (id: string, dpPath: string) {
|
||||
if (autoCreateAlias) {
|
||||
if (isSetOptionActive) {
|
||||
if (existsState(dpPath + 'Daten') == false) {
|
||||
await createStateAsync(dpPath + 'Daten', 'WIFI:T:undefined;S:undefined;P:undefined;H:undefined;', {type: 'string', write: true});
|
||||
await createStateAsync(dpPath + 'Daten', 'WIFI:T:undefined;S:undefined;P:undefined;H:;', {type: 'string', write: true});
|
||||
await createStateAsync(dpPath + 'Switch', false, {type: 'boolean', write: true});
|
||||
setObject(id, {_id: id, type: 'channel', common: {role: 'switch.mode.wlan', name: 'QR Page'}, native: {}});
|
||||
await createAliasAsync(id + '.ACTUAL', dpPath + 'Daten', true, {type: 'string', role: 'state', name: 'ACTUAL'});
|
||||
@@ -8642,7 +8714,7 @@ function GenerateQRPage (page: NSPanel.PageQR): NSPanel.Payload[] {
|
||||
let o = getObject(id);
|
||||
|
||||
let heading = page.heading !== undefined ? page.heading : typeof o.common.name === 'object' ? o.common.name.de : o.common.name;
|
||||
let textQR = page.items[0].id + '.ACTUAL' !== undefined ? getState(page.items[0].id + '.ACTUAL').val : 'WIFI:T:undefined;S:undefined;P:undefined;H:undefined;';
|
||||
let textQR = page.items[0].id + '.ACTUAL' !== undefined ? getState(page.items[0].id + '.ACTUAL').val : 'WIFI:T:undefined;S:undefined;P:undefined;H:;';
|
||||
let hiddenPWD = false;
|
||||
if (page.items[0].hidePassword !== undefined && page.items[0].hidePassword == true) {
|
||||
hiddenPWD = true;
|
||||
@@ -8780,7 +8852,11 @@ function unsubscribePowerSubscriptions (): void {
|
||||
* @returns {void}
|
||||
*/
|
||||
function subscribePowerSubscriptions (id: string): void {
|
||||
on({id: id + '.ACTUAL', change: 'ne'}, async function () {
|
||||
const subscriptionKey = 'power_' + id + '.ACTUAL';
|
||||
if (subscriptions.hasOwnProperty(subscriptionKey)) {
|
||||
return;
|
||||
}
|
||||
subscriptions[subscriptionKey] = on({id: id + '.ACTUAL', change: 'ne'}, async function () {
|
||||
(function () {
|
||||
if (timeoutPower) {
|
||||
clearTimeout(timeoutPower);
|
||||
@@ -8793,7 +8869,6 @@ function subscribePowerSubscriptions (id: string): void {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @function GeneratePowerPage
|
||||
* @description Generates a page with power state and energy usage information.
|
||||
@@ -9331,10 +9406,10 @@ function HandleButtonEvent (words: any): void {
|
||||
}
|
||||
break;
|
||||
case 'notifyAction':
|
||||
if (words[4] == 'yes') {
|
||||
if (words[4] == 'button1') { //Changes button1 retuns "button1" instead of "yes"
|
||||
setState(popupNotifyInternalName, {val: words[2], ack: true});
|
||||
setState(popupNotifyAction, {val: true, ack: true});
|
||||
} else if (words[4] == 'no') {
|
||||
} else if (words[4] == 'button3') { //Changes button3 retuns "button3" instead of "no" --> button2 has no functionality in Script (only Adapter)
|
||||
setState(popupNotifyInternalName, {val: words[2], ack: true});
|
||||
setState(popupNotifyAction, {val: false, ack: true});
|
||||
}
|
||||
@@ -10119,6 +10194,10 @@ function HandleButtonEvent (words: any): void {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let pageItemSlider = findPageItem(id);
|
||||
let sliderPos = Math.trunc(scale(parseInt(words[4]), 0, 100, pageItemSlider.maxValueLevel ? pageItemSlider.maxValue : 100, pageItemSlider.minValueLevel ? pageItemSlider.minValue : 0));
|
||||
setIfExists(pageItemSlider.id + '.SET', sliderPos) ? true : setIfExists(id + '.ACTUAL', sliderPos);
|
||||
}
|
||||
break;
|
||||
case 'mode-seek':
|
||||
@@ -10434,7 +10513,11 @@ function HandleButtonEvent (words: any): void {
|
||||
break;
|
||||
}
|
||||
} catch (err: any) {
|
||||
log('error at function HandleButtonEvent: ' + err.message, 'warn');
|
||||
if (err.message == "Cannot read properties of undefined (reading 'id')") {
|
||||
|
||||
} else {
|
||||
log('error at function HandleButtonEvent: ' + err.message, 'warn');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11375,7 +11458,7 @@ function GenerateDetailPage (type: NSPanel.PopupType, optional: NSPanel.mediaOpt
|
||||
let hSlider2MinVal: number = pageItem.minValue ?? 0;
|
||||
let hSlider2MaxVal: number = pageItem.maxValue ?? 100;
|
||||
let hSlider2ZeroVal: number = 0;
|
||||
let hSlider2CurVal: number = getState(id + '.ACTUAL').val;
|
||||
let hSlider2CurVal: number = existsState(id + '.ACTUAL') ? getState(id + '.ACTUAL').val : getState(id + '.SET').val;
|
||||
let hSlider2Step: number = 1;
|
||||
let hSlider2Visibility: string = "enable";
|
||||
|
||||
@@ -12391,85 +12474,100 @@ function HandleScreensaverUpdate (): void {
|
||||
} else if (weatherAdapterInstance == 'openweathermap.' + weatherAdapterInstanceNumber + '.') {
|
||||
entityIcon = Icons.GetIcon(GetOpenWeatherMapIcon(icon));
|
||||
entityIconCol = GetOpenWeatherMapIconColor(icon);
|
||||
} else if (weatherAdapterInstance == 'swiss-weather-api.' + weatherAdapterInstanceNumber + '.') {
|
||||
entityIcon = Icons.GetIcon(GetSwissWeatherApiIcon(icon));
|
||||
entityIconCol = GetSwissWeatherApiIconColor(icon);
|
||||
} else if (weatherAdapterInstance == 'pirate-weather.' + weatherAdapterInstanceNumber + '.' || weatherAdapterInstance == 'brightsky.' + weatherAdapterInstanceNumber + '.') {
|
||||
entityIcon = Icons.GetIcon(GetPirateWeatherIcon(icon));
|
||||
entityIconCol = GetPirateWeatherIconColor(icon);
|
||||
} else if (weatherAdapterInstance == 'brightsky.' + weatherAdapterInstanceNumber + '.') {
|
||||
entityIcon = Icons.GetIcon(icon);
|
||||
entityIconCol = GetBrightskyWeatherIconColor(icon);
|
||||
}
|
||||
|
||||
payloadString += '~' + '~' + entityIcon + '~' + entityIconCol + '~' + '~' + optionalValue + '~';
|
||||
}
|
||||
|
||||
// 3 leftScreensaverEntities
|
||||
if (screensaverAdvanced) {
|
||||
let checkpoint = true;
|
||||
let i = 0;
|
||||
if (config.leftScreensaverEntity && Array.isArray(config.leftScreensaverEntity) && config.leftScreensaverEntity.length > 0) {
|
||||
for (i = 0; i < 3 && i < config.leftScreensaverEntity.length; i++) {
|
||||
const leftScreensaverEntity = config.leftScreensaverEntity[i];
|
||||
if (leftScreensaverEntity === null || leftScreensaverEntity === undefined) {
|
||||
checkpoint = false;
|
||||
break;
|
||||
}
|
||||
RegisterScreensaverEntityWatcher(leftScreensaverEntity.ScreensaverEntity);
|
||||
|
||||
let val = getState(leftScreensaverEntity.ScreensaverEntity).val;
|
||||
let iconColor = rgb_dec565(White);
|
||||
let icon;
|
||||
if (typeof leftScreensaverEntity.ScreensaverEntityIconOn == 'string' && existsObject(leftScreensaverEntity.ScreensaverEntityIconOn as string)) {
|
||||
let iconName = getState(leftScreensaverEntity.ScreensaverEntityIconOn!).val;
|
||||
icon = Icons.GetIcon(iconName);
|
||||
} else {
|
||||
icon = Icons.GetIcon(leftScreensaverEntity.ScreensaverEntityIconOn);
|
||||
}
|
||||
|
||||
if (parseFloat(val + '') == val) {
|
||||
val = parseFloat(val);
|
||||
}
|
||||
|
||||
if (typeof val == 'number') {
|
||||
val = val * (leftScreensaverEntity.ScreensaverEntityFactor ? leftScreensaverEntity.ScreensaverEntityFactor! : 0)
|
||||
icon = determineScreensaverStatusIcon(leftScreensaverEntity,val,icon)
|
||||
val = val.toFixed(
|
||||
leftScreensaverEntity.ScreensaverEntityDecimalPlaces
|
||||
) + leftScreensaverEntity.ScreensaverEntityUnitText;
|
||||
iconColor = GetScreenSaverEntityColor(leftScreensaverEntity);
|
||||
} else if (typeof val == 'boolean') {
|
||||
iconColor = GetScreenSaverEntityColor(leftScreensaverEntity);
|
||||
if (!val && leftScreensaverEntity.ScreensaverEntityIconOff != null) {
|
||||
icon = Icons.GetIcon(leftScreensaverEntity.ScreensaverEntityIconOff);
|
||||
}
|
||||
} else if (typeof val == 'string') {
|
||||
iconColor = GetScreenSaverEntityColor(leftScreensaverEntity);
|
||||
let pformat = parseFormat(val);
|
||||
if (Debug) log('moments.js --> Datum ' + val + ' valid?: ' + moment(val, pformat, true).isValid(), 'info');
|
||||
if (moment(val, pformat, true).isValid()) {
|
||||
let DatumZeit = moment(val, pformat).unix(); // Umwandlung in Unix Time-Stamp
|
||||
if (leftScreensaverEntity.ScreensaverEntityDateFormat !== undefined) {
|
||||
val = new Date(DatumZeit * 1000).toLocaleString(getState(NSPanel_Path + 'Config.locale').val, leftScreensaverEntity.ScreensaverEntityDateFormat);
|
||||
} else {
|
||||
val = new Date(DatumZeit * 1000).toLocaleString(getState(NSPanel_Path + 'Config.locale').val);
|
||||
}
|
||||
}
|
||||
}
|
||||
const temp = leftScreensaverEntity.ScreensaverEntityIconColor;
|
||||
if (temp && typeof temp == 'string' && existsObject(temp)) {
|
||||
iconColor = getState(temp).val;
|
||||
}
|
||||
|
||||
payloadString += '~' + '~' + icon + '~' + iconColor + '~' + leftScreensaverEntity.ScreensaverEntityText + '~' + val + '~';
|
||||
}
|
||||
}
|
||||
|
||||
if (i < 3) {
|
||||
checkpoint = false;
|
||||
}
|
||||
|
||||
if (checkpoint == false) {
|
||||
for (let j = i; j < 3; j++) {
|
||||
payloadString += '~~~~~~';
|
||||
}
|
||||
}
|
||||
}
|
||||
if (screensaverAdvanced) {
|
||||
let checkpoint = true;
|
||||
let i = 0;
|
||||
if (config.leftScreensaverEntity && Array.isArray(config.leftScreensaverEntity) && config.leftScreensaverEntity.length > 0) {
|
||||
for (i = 0; i < 3 && i < config.leftScreensaverEntity.length; i++) {
|
||||
const leftScreensaverEntity = config.leftScreensaverEntity[i];
|
||||
if (leftScreensaverEntity === null || leftScreensaverEntity === undefined) {
|
||||
checkpoint = false;
|
||||
break;
|
||||
}
|
||||
RegisterScreensaverEntityWatcher(leftScreensaverEntity.ScreensaverEntity);
|
||||
|
||||
let val = getState(leftScreensaverEntity.ScreensaverEntity).val;
|
||||
let iconColor = rgb_dec565(White);
|
||||
let icon;
|
||||
if (typeof leftScreensaverEntity.ScreensaverEntityIconOn == 'string' && existsObject(leftScreensaverEntity.ScreensaverEntityIconOn as string)) {
|
||||
let iconName = getState(leftScreensaverEntity.ScreensaverEntityIconOn!).val;
|
||||
icon = Icons.GetIcon(iconName);
|
||||
} else {
|
||||
icon = Icons.GetIcon(leftScreensaverEntity.ScreensaverEntityIconOn);
|
||||
}
|
||||
|
||||
if (parseFloat(val + '') == val) {
|
||||
val = parseFloat(val);
|
||||
}
|
||||
|
||||
if (typeof val == 'number') {
|
||||
val = val * (leftScreensaverEntity.ScreensaverEntityFactor ? leftScreensaverEntity.ScreensaverEntityFactor! : 0)
|
||||
icon = determineScreensaverStatusIcon(leftScreensaverEntity, val, icon)
|
||||
|
||||
// Einheit ermitteln: String oder aus DP
|
||||
let unitText = '';
|
||||
if (typeof leftScreensaverEntity.ScreensaverEntityUnitText === 'string') {
|
||||
if (existsObject(leftScreensaverEntity.ScreensaverEntityUnitText)) {
|
||||
unitText = getState(leftScreensaverEntity.ScreensaverEntityUnitText).val;
|
||||
} else {
|
||||
unitText = leftScreensaverEntity.ScreensaverEntityUnitText;
|
||||
}
|
||||
}
|
||||
|
||||
val = val.toFixed(leftScreensaverEntity.ScreensaverEntityDecimalPlaces) + unitText;
|
||||
iconColor = GetScreenSaverEntityColor(leftScreensaverEntity);
|
||||
} else if (typeof val == 'boolean') {
|
||||
iconColor = GetScreenSaverEntityColor(leftScreensaverEntity);
|
||||
if (!val && leftScreensaverEntity.ScreensaverEntityIconOff != null) {
|
||||
icon = Icons.GetIcon(leftScreensaverEntity.ScreensaverEntityIconOff);
|
||||
}
|
||||
} else if (typeof val == 'string') {
|
||||
iconColor = GetScreenSaverEntityColor(leftScreensaverEntity);
|
||||
let pformat = parseFormat(val);
|
||||
if (Debug) log('moments.js --> Datum ' + val + ' valid?: ' + moment(val, pformat, true).isValid(), 'info');
|
||||
if (moment(val, pformat, true).isValid()) {
|
||||
let DatumZeit = moment(val, pformat).unix(); // Umwandlung in Unix Time-Stamp
|
||||
if (leftScreensaverEntity.ScreensaverEntityDateFormat !== undefined) {
|
||||
val = new Date(DatumZeit * 1000).toLocaleString(getState(NSPanel_Path + 'Config.locale').val, leftScreensaverEntity.ScreensaverEntityDateFormat);
|
||||
} else {
|
||||
val = new Date(DatumZeit * 1000).toLocaleString(getState(NSPanel_Path + 'Config.locale').val);
|
||||
}
|
||||
}
|
||||
}
|
||||
const temp = leftScreensaverEntity.ScreensaverEntityIconColor;
|
||||
if (temp && typeof temp == 'string' && existsObject(temp)) {
|
||||
iconColor = getState(temp).val;
|
||||
}
|
||||
|
||||
payloadString += '~' + '~' + icon + '~' + iconColor + '~' + leftScreensaverEntity.ScreensaverEntityText + '~' + val + '~';
|
||||
}
|
||||
}
|
||||
|
||||
if (i < 3) {
|
||||
checkpoint = false;
|
||||
}
|
||||
|
||||
if (checkpoint == false) {
|
||||
for (let j = i; j < 3; j++) {
|
||||
payloadString += '~~~~~~';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 6 bottomScreensaverEntities
|
||||
let maxEntities: number = 7;
|
||||
@@ -12594,17 +12692,41 @@ function HandleScreensaverUpdate (): void {
|
||||
DayOfWeek = existsObject('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.timestamp')
|
||||
? formatDate(getDateObject((getState('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.timestamp').val)), 'W', 'de')
|
||||
: 0;
|
||||
WeatherIcon = existsObject('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.icon')
|
||||
? GetPirateWeatherIcon(getState('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.icon').val)
|
||||
WeatherIcon = existsObject('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.icon_special')
|
||||
? getState('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.icon_special').val
|
||||
: '';
|
||||
WheatherColor = existsObject('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.icon')
|
||||
? GetPirateWeatherIconColor(String(getState('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.icon').val))
|
||||
WheatherColor = existsObject('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.icon_special')
|
||||
? GetBrightskyWeatherIconColor(String(getState('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.icon_special').val))
|
||||
: 0;
|
||||
|
||||
RegisterScreensaverEntityWatcher('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.temperature_min');
|
||||
RegisterScreensaverEntityWatcher('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.temperature_max');
|
||||
RegisterScreensaverEntityWatcher('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.timestamp');
|
||||
RegisterScreensaverEntityWatcher('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.icon');
|
||||
RegisterScreensaverEntityWatcher('brightsky.' + weatherAdapterInstanceNumber + '.daily.0' + String(i-1) + '.icon_special');
|
||||
}
|
||||
} else if (weatherAdapterInstance == 'swiss-weather-api.' + weatherAdapterInstanceNumber + '.') {
|
||||
if (i < 6) {
|
||||
// swiss-weather-api. 0 .forecast.days.day 0 .0000.TN_C
|
||||
TempMin = existsObject('swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day' + String(i - 1) + '.0000.TN_C')
|
||||
? Math.round(getState('swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day' + String(i - 1) + '.0000.TN_C').val * 10) / 10
|
||||
: 0;
|
||||
TempMax = existsObject('swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day' + String(i - 1) + '.0000.TX_C')
|
||||
? Math.round(getState('swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day' + String(i - 1) + '.0000.TX_C').val * 10) / 10
|
||||
: 0;
|
||||
DayOfWeek = existsObject('swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day' + String(i - 1) + '.0000.day_name')
|
||||
? getState('swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day' + String(i - 1) + '.0000.day_name').val
|
||||
: 0;
|
||||
WeatherIcon = existsObject('swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day' + String(i - 1) + '.0000.symbol_code')
|
||||
? GetSwissWeatherApiIcon(String(getState('swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day' + String(i - 1) + '.0000.symbol_code').val))
|
||||
: '';
|
||||
WheatherColor = existsObject('swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day' + String(i - 1) + '.0000.symbol_code')
|
||||
? GetSwissWeatherApiIconColor(String(getState('swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day' + String(i - 1) + '.0000.symbol_code').val))
|
||||
: 0;
|
||||
|
||||
RegisterScreensaverEntityWatcher('swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day' + String(i - 1) + '.0000.TN_C');
|
||||
RegisterScreensaverEntityWatcher('swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day' + String(i - 1) + '.0000.TX_C');
|
||||
RegisterScreensaverEntityWatcher('swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day' + String(i - 1) + '.0000.day_name');
|
||||
RegisterScreensaverEntityWatcher('swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day' + String(i - 1) + '.0000.symbol_code');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12714,6 +12836,30 @@ function HandleScreensaverUpdate (): void {
|
||||
sun = 'weather-sunset-up';
|
||||
}
|
||||
|
||||
payloadString += '~' + '~' + Icons.GetIcon(sun) + '~' + rgb_dec565(MSYellow) + '~' + 'Sonne' + '~' + formatDate(getDateObject(arraySunEvent[nextSunEvent]), 'hh:mm') + '~';
|
||||
} else if (weatherAdapterInstance == 'swiss-weather-api.' + weatherAdapterInstanceNumber + '.' && i == 6) {
|
||||
let nextSunEvent = 0;
|
||||
let valDateNow = getDateObject((new Date().getTime())).getTime();
|
||||
let arraySunEvent: number[] = [];
|
||||
|
||||
arraySunEvent[0] = getDateObject(getState('swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day0.0000.SUNRISE').val).getTime();
|
||||
arraySunEvent[1] = getDateObject(getState('swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day0.0000.SUNSET').val).getTime();
|
||||
arraySunEvent[2] = getDateObject(getState('swiss-weather-api.' + weatherAdapterInstanceNumber + '.forecast.days.day1.0000.SUNRISE').val).getTime();
|
||||
|
||||
let j = 0;
|
||||
for (j = 0; j < 3; j++) {
|
||||
if (arraySunEvent[j] > valDateNow) {
|
||||
nextSunEvent = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
let sun = '';
|
||||
if (j == 1) {
|
||||
sun = 'weather-sunset-down';
|
||||
} else {
|
||||
sun = 'weather-sunset-up';
|
||||
}
|
||||
|
||||
payloadString += '~' + '~' + Icons.GetIcon(sun) + '~' + rgb_dec565(MSYellow) + '~' + 'Sonne' + '~' + formatDate(getDateObject(arraySunEvent[nextSunEvent]), 'hh:mm') + '~';
|
||||
} else {
|
||||
payloadString += '~' + '~' + Icons.GetIcon(WeatherIcon) + '~' + WheatherColor + '~' + DayOfWeek + '~' + tempMinMaxString + '~';
|
||||
@@ -13686,6 +13832,193 @@ function GetOpenWeatherMapIconColor (icon: string): number {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the SwissWeatherApi icon string based on the provided icon string.
|
||||
*
|
||||
* This function maps the given SwissWeatherApi icon string to its corresponding icon string representation.
|
||||
* See https://github.com/baerengraben/ioBroker.swiss-weather-api/tree/master/img/Meteo_API_Icons/Color for
|
||||
* list of icons.
|
||||
*
|
||||
* @function GetSwissWeatherApiIcon
|
||||
* @param {string} icon - The icon string.
|
||||
* @returns {string} The corresponding icon string.
|
||||
*/
|
||||
function GetSwissWeatherApiIcon(icon: string): string {
|
||||
try {
|
||||
switch (icon) {
|
||||
case "1":
|
||||
return 'weather-sunny';
|
||||
case "-1":
|
||||
return 'weather-night';
|
||||
case "3": //few clouds day
|
||||
return 'weather-partly-cloudy';
|
||||
case "-3": //few clouds night
|
||||
return 'weather-night-partly-cloudy';
|
||||
case "10": //scattered clouds
|
||||
case "-10":
|
||||
return 'weather-cloudy';
|
||||
case "18": //cloudy
|
||||
case "-18":
|
||||
case "19":
|
||||
case "-19":
|
||||
return 'weather-cloudy';
|
||||
case "23": //shower rain
|
||||
case "-23":
|
||||
return 'weather-rainy';
|
||||
case "4": //rain
|
||||
case "-4":
|
||||
case "8":
|
||||
case "-8":
|
||||
case "11":
|
||||
case "-11":
|
||||
case "15":
|
||||
case "-15":
|
||||
case "20":
|
||||
case "-20":
|
||||
case "22":
|
||||
case "-22":
|
||||
case "25":
|
||||
case "-25":
|
||||
case "29":
|
||||
case "-29":
|
||||
return 'weather-pouring';
|
||||
case "5": //Thunderstorm
|
||||
case "-5":
|
||||
case "7":
|
||||
case "-7":
|
||||
case "9":
|
||||
case "-9":
|
||||
case "12":
|
||||
case "-12":
|
||||
case "14":
|
||||
case "-14":
|
||||
case "16":
|
||||
case "-16":
|
||||
case "26":
|
||||
case "-26":
|
||||
case "28":
|
||||
case "-28":
|
||||
case "30":
|
||||
case "-30":
|
||||
return 'weather-lightning';
|
||||
case "6": //snow
|
||||
case "-6":
|
||||
case "13":
|
||||
case "-13":
|
||||
case "21":
|
||||
case "-21":
|
||||
case "24":
|
||||
case "-24":
|
||||
case "27":
|
||||
case "-27":
|
||||
return 'weather-snowy';
|
||||
case "2": //mist
|
||||
case "-2":
|
||||
case "17":
|
||||
case "-17":
|
||||
return 'weather-fog';
|
||||
default:
|
||||
return 'alert-circle-outline';
|
||||
}
|
||||
} catch (err: any) {
|
||||
log('error at function GetSwissWeatherApiIcon: ' + err.message, 'warn');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the color code for a given SwissWeatherApi icon string.
|
||||
*
|
||||
* This function maps the provided SwissWeatherApi icon string to its corresponding color code.
|
||||
* See https://github.com/baerengraben/ioBroker.swiss-weather-api/tree/master/img/Meteo_API_Icons/Color for
|
||||
* list of icons.
|
||||
*
|
||||
* @function GetSwissWeatherApiIconColor
|
||||
* @param {string} icon - The icon string.
|
||||
* @returns {number} The corresponding color code.
|
||||
*/
|
||||
function GetSwissWeatherApiIconColor(icon: string): number {
|
||||
try {
|
||||
switch (icon) {
|
||||
case "1": //clear sky day
|
||||
return rgb_dec565(swSunny);
|
||||
case "-1": //clear sky night
|
||||
return rgb_dec565(swClearNight);
|
||||
case "3": //few clouds day
|
||||
case "-3": //few clouds night
|
||||
return rgb_dec565(swPartlycloudy);
|
||||
case "10": //scattered clouds
|
||||
case "-10":
|
||||
return rgb_dec565(swCloudy);
|
||||
case "18": //cloudy
|
||||
case "-18":
|
||||
case "19":
|
||||
case "-19":
|
||||
return rgb_dec565(swCloudy);
|
||||
case "23": //shower rain
|
||||
case "-23":
|
||||
return rgb_dec565(swRainy);
|
||||
case "4": //rain
|
||||
case "-4":
|
||||
case "8":
|
||||
case "-8":
|
||||
case "11":
|
||||
case "-11":
|
||||
case "15":
|
||||
case "-15":
|
||||
case "20":
|
||||
case "-20":
|
||||
case "22":
|
||||
case "-22":
|
||||
case "25":
|
||||
case "-25":
|
||||
case "29":
|
||||
case "-29":
|
||||
return rgb_dec565(swPouring);
|
||||
case "5": //Thunderstorm
|
||||
case "-5":
|
||||
case "7":
|
||||
case "-7":
|
||||
case "9":
|
||||
case "-9":
|
||||
case "12":
|
||||
case "-12":
|
||||
case "14":
|
||||
case "-14":
|
||||
case "16":
|
||||
case "-16":
|
||||
case "26":
|
||||
case "-26":
|
||||
case "28":
|
||||
case "-28":
|
||||
case "30":
|
||||
case "-30":
|
||||
return rgb_dec565(swLightningRainy);
|
||||
case "6": //snow
|
||||
case "-6":
|
||||
case "13":
|
||||
case "-13":
|
||||
case "21":
|
||||
case "-21":
|
||||
case "24":
|
||||
case "-24":
|
||||
case "27":
|
||||
case "-27":
|
||||
return rgb_dec565(swSnowy);
|
||||
case "2": //mist
|
||||
case "-2":
|
||||
case "17":
|
||||
case "-17":
|
||||
return rgb_dec565(swFog);
|
||||
default:
|
||||
return rgb_dec565(White);
|
||||
}
|
||||
} catch (err: any) {
|
||||
log('error at function GetSwissWeatherApiIconColor: ' + err.message, 'warn');
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the PirateWeather icon string based on the provided icon string.
|
||||
*
|
||||
@@ -13875,6 +14208,82 @@ function GetPirateWeatherIconColor (icon: string): number {
|
||||
return 0;
|
||||
}
|
||||
|
||||
function GetBrightskyWeatherIconColor (icon: string): number {
|
||||
try {
|
||||
switch (icon) {
|
||||
|
||||
case 'weather-cloudy':
|
||||
return rgb_dec565(swCloudy); // cloudy
|
||||
|
||||
case 'weather-fog':
|
||||
return rgb_dec565(swFog);
|
||||
|
||||
case 'weather-hail':
|
||||
return rgb_dec565(swHail);
|
||||
|
||||
case 'weather-hazy':
|
||||
return rgb_dec565(swFog);
|
||||
|
||||
case 'weather-lightning':
|
||||
return rgb_dec565(swLightning);
|
||||
|
||||
case 'weather-lightning-rainy':
|
||||
return rgb_dec565(swLightningRainy);
|
||||
|
||||
case 'weather-night':
|
||||
return rgb_dec565(swClearNight);
|
||||
|
||||
case 'weather-night-partly-cloudy':
|
||||
return rgb_dec565(swPartlycloudy);
|
||||
|
||||
case 'weather-partly-cloudy':
|
||||
return rgb_dec565(swPartlycloudy);
|
||||
|
||||
case 'weather-partly-rainy':
|
||||
return rgb_dec565(swRainy);
|
||||
|
||||
case 'weather-partly-snowy':
|
||||
return rgb_dec565(swSnowy);
|
||||
|
||||
case 'weather-partly-snowy-rainy':
|
||||
return rgb_dec565(swSnowyRainy);
|
||||
|
||||
case 'weather-pouring':
|
||||
return rgb_dec565(swPouring);
|
||||
|
||||
case 'weather-rainy':
|
||||
return rgb_dec565(swRainy);
|
||||
|
||||
case 'weather-snowy':
|
||||
return rgb_dec565(swSnowy);
|
||||
|
||||
case 'weather-snowy-heavy':
|
||||
return rgb_dec565(swSnowy);
|
||||
|
||||
case 'weather-snowy-rainy':
|
||||
return rgb_dec565(swSnowyRainy);
|
||||
|
||||
case 'weather-sunny':
|
||||
return rgb_dec565(swSunny);
|
||||
|
||||
case 'weather-tornado':
|
||||
return rgb_dec565(swExceptional);
|
||||
|
||||
case 'weather-windy':
|
||||
return rgb_dec565(swWindy);
|
||||
|
||||
case 'weather-windy-variant':
|
||||
return rgb_dec565(swWindy);
|
||||
|
||||
default:
|
||||
return rgb_dec565(swExceptional);
|
||||
}
|
||||
} catch (err: any) {
|
||||
log('error at function GetPirateWeatherIcon: ' + err.message, 'warn');
|
||||
}
|
||||
return rgb_dec565(swExceptional);
|
||||
}
|
||||
|
||||
//------------------Begin Read Internal Sensor Data
|
||||
//mqttCallback (topic: string, message: string): Promise<void> {
|
||||
/**
|
||||
@@ -14564,6 +14973,7 @@ function isEventMethod (F: string | NSPanel.EventMethod): F is NSPanel.EventMeth
|
||||
case 'sleepReached':
|
||||
case 'pageOpenDetail':
|
||||
case 'buttonPress2':
|
||||
case 'buttonPress3':
|
||||
case 'renderCurrentPage':
|
||||
case 'button1':
|
||||
case 'button2':
|
||||
@@ -14628,7 +15038,7 @@ function isPagePower (F: NSPanel.PageType | NSPanel.PagePower): F is NSPanel.Pag
|
||||
namespace NSPanel {
|
||||
export type PopupType = 'popupFan' | 'popupInSel' | 'popupLight' | 'popupNotify' | 'popupShutter' | 'popupShutter2' | 'popupSlider' | 'popupThermo' | 'popupTimer' | 'popupColor';
|
||||
|
||||
export type EventMethod = 'startup' | 'sleepReached' | 'pageOpenDetail' | 'buttonPress2' | 'renderCurrentPage' | 'button1' | 'button2';
|
||||
export type EventMethod = 'startup' | 'sleepReached' | 'pageOpenDetail' | 'buttonPress2' | 'buttonPress3' | 'renderCurrentPage' | 'button1' | 'button2';
|
||||
export type panelRecvType = {
|
||||
event: 'event';
|
||||
method: EventMethod;
|
||||
|
||||
@@ -12,7 +12,7 @@ repo_name: jobr99/nspanel-lovelace-ui
|
||||
repo_url: https://github.com/jobr99/nspanel-lovelace-ui
|
||||
edit_uri: ""
|
||||
|
||||
copyright: "Copyright © 2023 Johannes Braun"
|
||||
copyright: "Copyright © 2026 Johannes Braun"
|
||||
|
||||
docs_dir: docs
|
||||
|
||||
@@ -42,8 +42,6 @@ extra_css:
|
||||
extra:
|
||||
analytics:
|
||||
provider: custom
|
||||
version:
|
||||
provider: mike
|
||||
|
||||
markdown_extensions:
|
||||
- admonition
|
||||
@@ -63,7 +61,6 @@ markdown_extensions:
|
||||
plugins:
|
||||
- search:
|
||||
lang: en
|
||||
- mkdocs-video
|
||||
|
||||
nav:
|
||||
- "Overview": index.md
|
||||
@@ -75,6 +72,7 @@ nav:
|
||||
- "FAQ": faq.md
|
||||
- "Configuration - apps.yaml (Home Assistant)":
|
||||
- "Overview": config-overview.md
|
||||
#- "Migration to Standalone Rewrite": config-migration-standalone.md
|
||||
- "Screensaver": config-screensaver.md
|
||||
- "Cards":
|
||||
- "Entities Card": card-entities.md
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# https://developers.home-assistant.io/docs/add-ons/configuration#add-on-config
|
||||
name: NSPanel Lovelace UI Addon
|
||||
version: "4.7.80"
|
||||
version: "4.7.85"
|
||||
slug: nspanel-lovelace-ui
|
||||
description: NSPanel Lovelace UI Addon
|
||||
services:
|
||||
|
||||
@@ -475,7 +475,7 @@ class AlarmCard(HACard):
|
||||
main_entity = self.entities[0]
|
||||
main_entity.render()
|
||||
|
||||
print(main_entity.state)
|
||||
logging.debug("Alarm card state for '%s': %s", main_entity.entity_id, main_entity.state)
|
||||
|
||||
icon = get_icon_char("shield-off")
|
||||
color = rgb_dec565([255,255,255])
|
||||
|
||||
@@ -8,6 +8,8 @@ def wait_for_ha_cache():
|
||||
while time.time() < mustend:
|
||||
if len(libs.home_assistant.home_assistant_entity_state_cache) == 0:
|
||||
time.sleep(0.1)
|
||||
if len(libs.home_assistant.home_assistant_entity_state_cache) == 0:
|
||||
logging.warning("Home Assistant entity cache is still empty after waiting 5 seconds")
|
||||
time.sleep(1)
|
||||
|
||||
def calculate_dim_values(sleepTracking, sleepTrackingZones, sleepBrightness, screenBrightness, sleepOverride, return_involved_entities=False):
|
||||
@@ -28,8 +30,8 @@ def calculate_dim_values(sleepTracking, sleepTrackingZones, sleepBrightness, scr
|
||||
involved_entities.append(sleepBrightness)
|
||||
try:
|
||||
dimmode = int(float(libs.home_assistant.get_entity_data(sleepBrightness).get('state', 10)))
|
||||
except ValueError:
|
||||
print("sleepBrightness entity invalid")
|
||||
except (TypeError, ValueError):
|
||||
logging.exception("sleepBrightness entity '%s' has an invalid state value", sleepBrightness)
|
||||
|
||||
if screenBrightness:
|
||||
if isinstance(screenBrightness, int):
|
||||
@@ -44,8 +46,8 @@ def calculate_dim_values(sleepTracking, sleepTrackingZones, sleepBrightness, scr
|
||||
involved_entities.append(screenBrightness)
|
||||
try:
|
||||
dimValueNormal = int(float(libs.home_assistant.get_entity_data(screenBrightness).get('state', 100)))
|
||||
except ValueError:
|
||||
print("screenBrightness entity invalid")
|
||||
except (TypeError, ValueError):
|
||||
logging.exception("screenBrightness entity '%s' has an invalid state value", screenBrightness)
|
||||
# force sleep brightness to zero in case sleepTracking is active
|
||||
if sleepTracking:
|
||||
if libs.home_assistant.is_existent(sleepTracking):
|
||||
@@ -69,12 +71,13 @@ def calculate_dim_values(sleepTracking, sleepTrackingZones, sleepBrightness, scr
|
||||
else:
|
||||
return dimmode, dimValueNormal
|
||||
|
||||
def handle_buttons(entity_id, btype, value, entity_config=None):
|
||||
def handle_buttons(entity_id, btype, value, entity_config=None, action_context=None):
|
||||
action_context = action_context or {}
|
||||
match btype:
|
||||
case 'button':
|
||||
button_press(entity_id, value)
|
||||
button_press(entity_id, value, action_context=action_context)
|
||||
case 'OnOff':
|
||||
on_off(entity_id, value)
|
||||
on_off(entity_id, value, action_context=action_context)
|
||||
case 'number-set':
|
||||
if entity_id.startswith('fan'):
|
||||
attr = libs.home_assistant.get_entity_data(entity_id).get('attributes', [])
|
||||
@@ -82,7 +85,7 @@ def handle_buttons(entity_id, btype, value, entity_config=None):
|
||||
service_data = {
|
||||
"value": int(value)
|
||||
}
|
||||
call_ha_service(entity_id, "set_value", service_data=service_data)
|
||||
call_ha_service(entity_id, "set_value", service_data=service_data, action_context=action_context)
|
||||
case 'up' | 'stop' | 'down' | 'tiltOpen' | 'tiltStop' | 'tiltClose' | 'media-next' | 'media-back' | 'media-pause' | 'timer-cancel' | 'timer-pause' | 'timer-finish':
|
||||
action_service_mapping = {
|
||||
'up': 'open_cover',
|
||||
@@ -99,37 +102,37 @@ def handle_buttons(entity_id, btype, value, entity_config=None):
|
||||
'timer-finish': 'finish',
|
||||
}
|
||||
service = action_service_mapping[btype]
|
||||
call_ha_service(entity_id, service)
|
||||
call_ha_service(entity_id, service, action_context=action_context)
|
||||
case 'timer-start':
|
||||
if value:
|
||||
service_data = {
|
||||
"duration": value
|
||||
}
|
||||
call_ha_service(entity_id, "start", service_data=service_data)
|
||||
call_ha_service(entity_id, "start", service_data=service_data, action_context=action_context)
|
||||
else:
|
||||
call_ha_service(entity_id, "start")
|
||||
call_ha_service(entity_id, "start", action_context=action_context)
|
||||
case 'positionSlider':
|
||||
service_data = {
|
||||
"position": int(value)
|
||||
}
|
||||
call_ha_service(entity_id, "set_cover_position", service_data=service_data)
|
||||
call_ha_service(entity_id, "set_cover_position", service_data=service_data, action_context=action_context)
|
||||
case 'tiltSlider':
|
||||
service_data = {
|
||||
"tilt_position": int(value)
|
||||
}
|
||||
call_ha_service(entity_id, "set_cover_tilt_position", service_data=service_data)
|
||||
call_ha_service(entity_id, "set_cover_tilt_position", service_data=service_data, action_context=action_context)
|
||||
case 'media-OnOff':
|
||||
state = libs.home_assistant.get_entity_data(entity_id).get('state', '')
|
||||
if state == "off":
|
||||
call_ha_service(entity_id, "turn_on")
|
||||
call_ha_service(entity_id, "turn_on", action_context=action_context)
|
||||
else:
|
||||
call_ha_service(entity_id, "turn_off")
|
||||
call_ha_service(entity_id, "turn_off", action_context=action_context)
|
||||
case 'media-shuffle':
|
||||
suffle = libs.home_assistant.get_entity_data(entity_id).get('attributes', []).get('shuffle')
|
||||
service_data = {
|
||||
"shuffle": not suffle
|
||||
}
|
||||
call_ha_service(entity_id, "set_value", service_data=service_data)
|
||||
call_ha_service(entity_id, "set_value", service_data=service_data, action_context=action_context)
|
||||
case 'volumeSlider':
|
||||
pos = int(value)
|
||||
# HA wants to have this value between 0 and 1 as float
|
||||
@@ -137,12 +140,12 @@ def handle_buttons(entity_id, btype, value, entity_config=None):
|
||||
service_data = {
|
||||
"volume_level": pos
|
||||
}
|
||||
call_ha_service(entity_id, "volume_set", service_data=service_data)
|
||||
call_ha_service(entity_id, "volume_set", service_data=service_data, action_context=action_context)
|
||||
case 'speaker-sel':
|
||||
service_data = {
|
||||
"volume_level": value
|
||||
}
|
||||
call_ha_service(entity_id, "select_source", service_data=service_data)
|
||||
call_ha_service(entity_id, "select_source", service_data=service_data, action_context=action_context)
|
||||
# for light detail page
|
||||
case 'brightnessSlider':
|
||||
# scale 0-100 to ha brightness range
|
||||
@@ -150,7 +153,7 @@ def handle_buttons(entity_id, btype, value, entity_config=None):
|
||||
service_data = {
|
||||
"brightness": brightness
|
||||
}
|
||||
call_ha_service(entity_id, "turn_on", service_data=service_data)
|
||||
call_ha_service(entity_id, "turn_on", service_data=service_data, action_context=action_context)
|
||||
case 'colorTempSlider':
|
||||
attr = libs.home_assistant.get_entity_data(entity_id).get('attributes', [])
|
||||
min_mireds = attr.get("min_mireds")
|
||||
@@ -160,19 +163,19 @@ def handle_buttons(entity_id, btype, value, entity_config=None):
|
||||
service_data = {
|
||||
"color_temp": color_val
|
||||
}
|
||||
call_ha_service(entity_id, "turn_on", service_data=service_data)
|
||||
call_ha_service(entity_id, "turn_on", service_data=service_data, action_context=action_context)
|
||||
case 'colorWheel':
|
||||
value = value.split('|')
|
||||
color = pos_to_color(int(value[0]), int(value[1]), int(value[2]))
|
||||
service_data = {
|
||||
"rgb_color": color
|
||||
}
|
||||
call_ha_service(entity_id, "turn_on", service_data=service_data)
|
||||
call_ha_service(entity_id, "turn_on", service_data=service_data, action_context=action_context)
|
||||
case 'disarm' | 'arm_home' | 'arm_away' | 'arm_night' | 'arm_vacation':
|
||||
service_data = {
|
||||
"code": value
|
||||
}
|
||||
call_ha_service(entity_id, f"alarm_{btype}", service_data=service_data)
|
||||
call_ha_service(entity_id, f"alarm_{btype}", service_data=service_data, action_context=action_context)
|
||||
case 'mode-preset_modes' | 'mode-swing_modes' | 'mode-fan_modes':
|
||||
attr = libs.home_assistant.get_entity_data(entity_id).get('attributes', [])
|
||||
mapping = {
|
||||
@@ -187,7 +190,7 @@ def handle_buttons(entity_id, btype, value, entity_config=None):
|
||||
service_data = {
|
||||
mapping[btype][:-1]: mode
|
||||
}
|
||||
call_ha_service(entity_id, f"set_{mapping[btype][:-1]}", service_data=service_data)
|
||||
call_ha_service(entity_id, f"set_{mapping[btype][:-1]}", service_data=service_data, action_context=action_context)
|
||||
case 'mode-input_select' | 'mode-select':
|
||||
options = libs.home_assistant.get_entity_data(entity_id).get('attributes', []).get("options", [])
|
||||
if options:
|
||||
@@ -195,7 +198,7 @@ def handle_buttons(entity_id, btype, value, entity_config=None):
|
||||
service_data = {
|
||||
"option": option
|
||||
}
|
||||
call_ha_service(entity_id, "select_option", service_data=service_data)
|
||||
call_ha_service(entity_id, "select_option", service_data=service_data, action_context=action_context)
|
||||
case 'mode-media_player':
|
||||
options = libs.home_assistant.get_entity_data(entity_id).get('attributes', []).get("source_list", [])
|
||||
if options:
|
||||
@@ -203,7 +206,7 @@ def handle_buttons(entity_id, btype, value, entity_config=None):
|
||||
service_data = {
|
||||
"source": option
|
||||
}
|
||||
call_ha_service(entity_id, "select_source", service_data=service_data)
|
||||
call_ha_service(entity_id, "select_source", service_data=service_data, action_context=action_context)
|
||||
case 'mode-light':
|
||||
options = entity_config.get("effectList", libs.home_assistant.get_entity_data(entity_id).get('attributes', []).get("effect_list", []))
|
||||
if options:
|
||||
@@ -211,13 +214,13 @@ def handle_buttons(entity_id, btype, value, entity_config=None):
|
||||
service_data = {
|
||||
"effect": option
|
||||
}
|
||||
call_ha_service(entity_id, "turn_on", service_data=service_data)
|
||||
call_ha_service(entity_id, "turn_on", service_data=service_data, action_context=action_context)
|
||||
case 'tempUpd':
|
||||
temp = int(value)/10
|
||||
service_data = {
|
||||
"temperature": temp
|
||||
}
|
||||
call_ha_service(entity_id, "set_temperature", service_data=service_data)
|
||||
call_ha_service(entity_id, "set_temperature", service_data=service_data, action_context=action_context)
|
||||
case 'tempUpdHighLow':
|
||||
value = value.split("|")
|
||||
temp_high = int(value[0])/10
|
||||
@@ -226,47 +229,67 @@ def handle_buttons(entity_id, btype, value, entity_config=None):
|
||||
"target_temp_high": temp_high,
|
||||
"target_temp_low": temp_low,
|
||||
}
|
||||
call_ha_service(entity_id, "set_temperature", service_data=service_data)
|
||||
call_ha_service(entity_id, "set_temperature", service_data=service_data, action_context=action_context)
|
||||
case 'hvac_action':
|
||||
service_data = {
|
||||
"hvac_mode": value
|
||||
}
|
||||
call_ha_service(entity_id, "set_hvac_mode", service_data=service_data)
|
||||
call_ha_service(entity_id, "set_hvac_mode", service_data=service_data, action_context=action_context)
|
||||
case _:
|
||||
logging.error("Not implemented: %s", btype)
|
||||
|
||||
def call_ha_service(entity_id, service, service_data = {}):
|
||||
def call_ha_service(entity_id, service, service_data=None, action_context=None):
|
||||
if service_data is None:
|
||||
service_data = {}
|
||||
action_context = action_context or {}
|
||||
etype = entity_id.split(".")[0]
|
||||
libs.home_assistant.call_service(
|
||||
ok = libs.home_assistant.call_service(
|
||||
entity_name=entity_id,
|
||||
domain=etype,
|
||||
service=service,
|
||||
service_data=service_data
|
||||
)
|
||||
if ok:
|
||||
logging.info(
|
||||
"Panel action forwarded to Home Assistant: panel='%s', action='%s', value='%s', entity='%s', service='%s', data=%s",
|
||||
action_context.get("panel", "unknown"),
|
||||
action_context.get("btype", "unknown"),
|
||||
action_context.get("value"),
|
||||
entity_id,
|
||||
service,
|
||||
service_data,
|
||||
)
|
||||
if not ok:
|
||||
logging.error(
|
||||
"Home Assistant service call failed: entity='%s', service='%s', data=%s",
|
||||
entity_id,
|
||||
service,
|
||||
service_data,
|
||||
)
|
||||
|
||||
def button_press(entity_id, value):
|
||||
def button_press(entity_id, value, action_context=None):
|
||||
etype = entity_id.split(".")[0]
|
||||
match etype:
|
||||
case 'scene' | 'script':
|
||||
call_ha_service(entity_id, "turn_on")
|
||||
call_ha_service(entity_id, "turn_on", action_context=action_context)
|
||||
case 'light' | 'switch' | 'input_boolean' | 'automation' | 'fan':
|
||||
call_ha_service(entity_id, "toggle")
|
||||
call_ha_service(entity_id, "toggle", action_context=action_context)
|
||||
case 'lock':
|
||||
state = libs.home_assistant.get_entity_data(entity_id).get('state', '')
|
||||
if state == "locked":
|
||||
call_ha_service(entity_id, "unlock")
|
||||
call_ha_service(entity_id, "unlock", action_context=action_context)
|
||||
else:
|
||||
call_ha_service(entity_id, "lock")
|
||||
call_ha_service(entity_id, "lock", action_context=action_context)
|
||||
case 'button' | 'input_button':
|
||||
call_ha_service(entity_id, "press")
|
||||
call_ha_service(entity_id, "press", action_context=action_context)
|
||||
case 'input_select' | 'select':
|
||||
call_ha_service(entity_id, "select_next")
|
||||
call_ha_service(entity_id, "select_next", action_context=action_context)
|
||||
case 'vacuum':
|
||||
state = libs.home_assistant.get_entity_data(entity_id).get('state', '')
|
||||
if state == "docked":
|
||||
call_ha_service(entity_id, "start")
|
||||
call_ha_service(entity_id, "start", action_context=action_context)
|
||||
else:
|
||||
call_ha_service(entity_id, "return_to_base")
|
||||
call_ha_service(entity_id, "return_to_base", action_context=action_context)
|
||||
case _:
|
||||
logging.error("buttonpress for entity type %s not implemented", etype)
|
||||
|
||||
@@ -274,14 +297,14 @@ def button_press(entity_id, value):
|
||||
# apis.ha_api.call_service(entity_id.replace(
|
||||
# 'service.', '', 1).replace('.', '/', 1), **entity_config.data)
|
||||
|
||||
def on_off(entity_id, value):
|
||||
def on_off(entity_id, value, action_context=None):
|
||||
etype = entity_id.split(".")[0]
|
||||
match etype:
|
||||
case 'light' | 'switch' | 'input_boolean' | 'automation' | 'fan':
|
||||
service = "turn_off"
|
||||
if value == "1":
|
||||
service = "turn_on"
|
||||
call_ha_service(entity_id, service)
|
||||
call_ha_service(entity_id, service, action_context=action_context)
|
||||
case _:
|
||||
logging.error(
|
||||
"Control action on_off not implemented for %s", entity_id)
|
||||
|
||||
@@ -14,8 +14,9 @@ next_id = 0
|
||||
request_all_states_id = 0
|
||||
ws_connected = False
|
||||
home_assistant_entity_state_cache = {}
|
||||
template_cache = {}
|
||||
response_buffer = {}
|
||||
template_cache = {}
|
||||
response_buffer = {}
|
||||
nspanel_event_handler = None
|
||||
|
||||
|
||||
ON_CONNECT_HANDLER = None
|
||||
@@ -44,47 +45,64 @@ def register_on_disconnect_handler(handler):
|
||||
ON_DISCONNECT_HANDLER = handler
|
||||
|
||||
|
||||
def on_message(ws, message):
|
||||
global auth_ok, request_all_states_id, home_assistant_entity_state_cache, response_buffer, template_cache
|
||||
json_msg = json.loads(message)
|
||||
if json_msg["type"] == "auth_required":
|
||||
authenticate_client()
|
||||
elif json_msg["type"] == "auth_ok":
|
||||
auth_ok = True
|
||||
logging.info("Home Assistant auth OK. Requesting existing states.")
|
||||
subscribe_to_events()
|
||||
_get_all_states()
|
||||
if ON_CONNECT_HANDLER is not None:
|
||||
ON_CONNECT_HANDLER()
|
||||
# for templates
|
||||
elif json_msg["type"] == "event" and json_msg["id"] in response_buffer:
|
||||
template_cache[response_buffer[json_msg["id"]]] = {
|
||||
"result": json_msg["event"]["result"],
|
||||
"listener-entities": json_msg["event"]["listeners"]["entities"]
|
||||
}
|
||||
elif json_msg["type"] == "event" and json_msg["event"]["event_type"] == "state_changed":
|
||||
entity_id = json_msg["event"]["data"]["entity_id"]
|
||||
home_assistant_entity_state_cache[entity_id] = json_msg["event"]["data"]["new_state"]
|
||||
send_entity_update(entity_id)
|
||||
# rerender template
|
||||
for template, template_cache_entry in template_cache.items():
|
||||
if entity_id in template_cache_entry.get("listener-entities", []):
|
||||
cache_template(template)
|
||||
elif json_msg["type"] == "event" and json_msg["event"]["event_type"] == "esphome.nspanel.data":
|
||||
nspanel_data_callback(json_msg["event"]["data"]["device_id"], json_msg["event"]["data"]["CustomRecv"])
|
||||
elif json_msg["type"] == "result" and not json_msg["success"]:
|
||||
logging.error("Failed result: ")
|
||||
logging.error(json_msg)
|
||||
elif json_msg["type"] == "result" and json_msg["success"]:
|
||||
if json_msg["id"] == request_all_states_id:
|
||||
for entity in json_msg["result"]:
|
||||
home_assistant_entity_state_cache[entity["entity_id"]] = entity
|
||||
else:
|
||||
if json_msg["id"] in response_buffer and json_msg.get("result"):
|
||||
response_buffer[json_msg["id"]] = json_msg["result"]
|
||||
return None # Ignore success result messages
|
||||
else:
|
||||
logging.debug(message)
|
||||
def on_message(ws, message):
|
||||
global auth_ok, request_all_states_id, home_assistant_entity_state_cache, response_buffer, template_cache
|
||||
try:
|
||||
json_msg = json.loads(message)
|
||||
except json.JSONDecodeError:
|
||||
logging.exception("Failed to parse Home Assistant websocket message as JSON")
|
||||
return
|
||||
|
||||
message_type = json_msg.get("type")
|
||||
if message_type == "auth_required":
|
||||
authenticate_client()
|
||||
elif message_type == "auth_ok":
|
||||
auth_ok = True
|
||||
logging.info("Home Assistant auth OK. Requesting existing states.")
|
||||
subscribe_to_events()
|
||||
_get_all_states()
|
||||
if ON_CONNECT_HANDLER is not None:
|
||||
ON_CONNECT_HANDLER()
|
||||
# for templates
|
||||
elif message_type == "event" and json_msg.get("id") in response_buffer:
|
||||
event = json_msg.get("event", {})
|
||||
listeners = event.get("listeners", {})
|
||||
template_cache[response_buffer[json_msg["id"]]] = {
|
||||
"result": event.get("result"),
|
||||
"listener-entities": listeners.get("entities", [])
|
||||
}
|
||||
elif message_type == "event" and json_msg.get("event", {}).get("event_type") == "state_changed":
|
||||
event_data = json_msg.get("event", {}).get("data", {})
|
||||
entity_id = event_data.get("entity_id")
|
||||
if not entity_id:
|
||||
logging.debug("Received state_changed event without entity_id")
|
||||
return
|
||||
home_assistant_entity_state_cache[entity_id] = event_data.get("new_state")
|
||||
send_entity_update(entity_id)
|
||||
# rerender template
|
||||
for template, template_cache_entry in template_cache.items():
|
||||
if entity_id in template_cache_entry.get("listener-entities", []):
|
||||
cache_template(template)
|
||||
elif message_type == "event" and json_msg.get("event", {}).get("event_type") == "esphome.nspanel.data":
|
||||
event_data = json_msg.get("event", {}).get("data", {})
|
||||
device_id = event_data.get("device_id")
|
||||
custom_recv = event_data.get("CustomRecv")
|
||||
if nspanel_event_handler is None:
|
||||
logging.debug("No NsPanel event handler registered; dropping event for device '%s'", device_id)
|
||||
return
|
||||
nspanel_event_handler(device_id, custom_recv)
|
||||
elif message_type == "result" and not json_msg.get("success"):
|
||||
logging.error("Home Assistant request failed: %s", json_msg)
|
||||
elif message_type == "result" and json_msg.get("success"):
|
||||
if json_msg.get("id") == request_all_states_id:
|
||||
for entity in json_msg.get("result", []):
|
||||
home_assistant_entity_state_cache[entity["entity_id"]] = entity
|
||||
else:
|
||||
if json_msg.get("id") in response_buffer and json_msg.get("result"):
|
||||
response_buffer[json_msg["id"]] = json_msg["result"]
|
||||
return None # Ignore success result messages
|
||||
else:
|
||||
logging.debug(message)
|
||||
|
||||
|
||||
def _ws_connection_open(ws):
|
||||
@@ -95,20 +113,24 @@ def _ws_connection_open(ws):
|
||||
ON_CONNECT_HANDLER()
|
||||
|
||||
|
||||
def _ws_connection_close(ws, close_status_code, close_msg):
|
||||
global ws_connected
|
||||
ws_connected = False
|
||||
logging.error("WebSocket connection closed!")
|
||||
if ON_DISCONNECT_HANDLER is not None:
|
||||
ON_DISCONNECT_HANDLER()
|
||||
def _ws_connection_close(ws, close_status_code, close_msg):
|
||||
global ws_connected
|
||||
ws_connected = False
|
||||
logging.error(
|
||||
"WebSocket connection closed (status=%s, message=%s)",
|
||||
close_status_code,
|
||||
close_msg,
|
||||
)
|
||||
if ON_DISCONNECT_HANDLER is not None:
|
||||
ON_DISCONNECT_HANDLER()
|
||||
|
||||
|
||||
def connect():
|
||||
Thread(target=_do_connection, daemon=True).start()
|
||||
|
||||
|
||||
def _do_connection():
|
||||
global home_assistant_url, ws, settings
|
||||
def _do_connection():
|
||||
global home_assistant_url, ws, settings
|
||||
ws_url = home_assistant_url.replace(
|
||||
"https://", "wss://").replace("http://", "ws://")
|
||||
if settings["is_addon"]:
|
||||
@@ -117,12 +139,15 @@ def _do_connection():
|
||||
ws_url += "/api/websocket"
|
||||
ws = websocket.WebSocketApp(F"{ws_url}", on_message=on_message,
|
||||
on_open=_ws_connection_open, on_close=_ws_connection_close)
|
||||
while True:
|
||||
logging.info(F"Connecting to Home Assistant at {ws_url}")
|
||||
ws.close()
|
||||
time.sleep(1)
|
||||
ws.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE})
|
||||
time.sleep(10)
|
||||
while True:
|
||||
logging.info(F"Connecting to Home Assistant at {ws_url}")
|
||||
try:
|
||||
ws.close()
|
||||
time.sleep(1)
|
||||
ws.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE})
|
||||
except Exception:
|
||||
logging.exception("WebSocket connection loop failed")
|
||||
time.sleep(10)
|
||||
|
||||
|
||||
def authenticate_client():
|
||||
@@ -144,9 +169,9 @@ def subscribe_to_events():
|
||||
}
|
||||
send_message(json.dumps(msg))
|
||||
|
||||
def subscribe_to_nspanel_events(nsp_callback):
|
||||
global next_id, nspanel_data_callback
|
||||
nspanel_data_callback = nsp_callback
|
||||
def subscribe_to_nspanel_events(nsp_callback):
|
||||
global next_id, nspanel_event_handler
|
||||
nspanel_event_handler = nsp_callback
|
||||
msg = {
|
||||
"id": next_id,
|
||||
"type": "subscribe_events",
|
||||
@@ -168,11 +193,13 @@ def send_entity_update(entity_id):
|
||||
global on_ha_update
|
||||
on_ha_update(entity_id)
|
||||
|
||||
def nspanel_data_callback(device_id, msg):
|
||||
global nspanel_data_callback
|
||||
nspanel_data_callback(device_id, msg)
|
||||
def nspanel_data_callback(device_id, msg):
|
||||
if nspanel_event_handler is None:
|
||||
logging.debug("NsPanel callback invoked before handler was registered")
|
||||
return
|
||||
nspanel_event_handler(device_id, msg)
|
||||
|
||||
def call_service(entity_name: str, domain: str, service: str, service_data: dict) -> bool:
|
||||
def call_service(entity_name: str, domain: str, service: str, service_data: dict) -> bool:
|
||||
global next_id
|
||||
try:
|
||||
msg = {
|
||||
@@ -187,9 +214,12 @@ def call_service(entity_name: str, domain: str, service: str, service_data: dict
|
||||
}
|
||||
send_message(json.dumps(msg))
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.exception("Failed to call Home Assisatant service.")
|
||||
return False
|
||||
except Exception:
|
||||
logging.exception(
|
||||
"Failed to call Home Assistant service: %s.%s for %s",
|
||||
domain, service, entity_name
|
||||
)
|
||||
return False
|
||||
|
||||
def send_msg_to_panel(service: str, service_data: dict) -> bool:
|
||||
global next_id
|
||||
@@ -203,9 +233,9 @@ def send_msg_to_panel(service: str, service_data: dict) -> bool:
|
||||
}
|
||||
send_message(json.dumps(msg))
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.exception("Failed to call Home Assisatant service.")
|
||||
return False
|
||||
except Exception:
|
||||
logging.exception("Failed to call Home Assistant panel service: %s", service)
|
||||
return False
|
||||
|
||||
def execute_script(entity_name: str, domain: str, service: str, service_data: dict) -> str:
|
||||
global next_id, response_buffer
|
||||
@@ -241,13 +271,13 @@ def execute_script(entity_name: str, domain: str, service: str, service_data: di
|
||||
else:
|
||||
return response_buffer[call_id]["response"]
|
||||
raise TimeoutError("Did not recive respose in time to HA script call")
|
||||
except Exception as e:
|
||||
logging.exception("Failed to call Home Assisatant script.")
|
||||
return {}
|
||||
except Exception:
|
||||
logging.exception("Failed to call Home Assistant script: %s.%s", domain, service)
|
||||
return {}
|
||||
|
||||
def cache_template(template):
|
||||
if not template:
|
||||
raise Exception("Invalid template")
|
||||
def cache_template(template):
|
||||
if not template:
|
||||
raise ValueError("Invalid template")
|
||||
global next_id, response_buffer
|
||||
try:
|
||||
call_id = next_id
|
||||
@@ -259,9 +289,9 @@ def cache_template(template):
|
||||
}
|
||||
send_message(json.dumps(msg))
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.exception("Failed to render template.")
|
||||
return False
|
||||
except Exception:
|
||||
logging.exception("Failed to render template.")
|
||||
return False
|
||||
|
||||
def get_template(template):
|
||||
global template_cache
|
||||
@@ -299,7 +329,12 @@ def is_existent(entity_id: str):
|
||||
return False
|
||||
|
||||
|
||||
def send_message(message):
|
||||
global ws, next_id
|
||||
next_id += 1
|
||||
ws.send(message)
|
||||
def send_message(message):
|
||||
global ws, next_id
|
||||
try:
|
||||
next_id += 1
|
||||
ws.send(message)
|
||||
except NameError:
|
||||
logging.error("WebSocket client is not initialized; dropping outgoing message")
|
||||
except Exception:
|
||||
logging.exception("Failed sending websocket message to Home Assistant")
|
||||
|
||||
@@ -25,53 +25,76 @@ last_settings_file_mtime = 0
|
||||
mqtt_connect_time = 0
|
||||
has_sent_reload_command = False
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
log_level_name = os.getenv("LOGLEVEL", "DEBUG").upper()
|
||||
log_level = getattr(logging, log_level_name, None)
|
||||
invalid_log_level = not isinstance(log_level, int)
|
||||
if invalid_log_level:
|
||||
log_level = logging.DEBUG
|
||||
|
||||
logging.basicConfig(
|
||||
level=log_level,
|
||||
format="%(asctime)s %(levelname)s [%(threadName)s] %(name)s: %(message)s",
|
||||
)
|
||||
if invalid_log_level:
|
||||
logging.warning("Invalid loglevel '%s', defaulting to DEBUG", log_level_name)
|
||||
|
||||
def on_ha_update(entity_id):
|
||||
global panel_in_queues
|
||||
# send HA updates to all panels
|
||||
for queue in panel_in_queues.values():
|
||||
queue.put(("HA:", entity_id))
|
||||
try:
|
||||
queue.put(("HA:", entity_id))
|
||||
except Exception:
|
||||
logging.exception("Failed to enqueue HA update for entity '%s'", entity_id)
|
||||
|
||||
def on_ha_panel_event(device_id, msg):
|
||||
global panel_in_queues
|
||||
|
||||
if device_id in panel_in_queues.keys():
|
||||
queue = panel_in_queues[device_id]
|
||||
queue.put(("MQTT:", msg))
|
||||
try:
|
||||
queue.put(("MQTT:", msg))
|
||||
except Exception:
|
||||
logging.exception("Failed to enqueue panel event for device '%s'", device_id)
|
||||
|
||||
def process_output_to_panel():
|
||||
while True:
|
||||
msg = panel_out_queue.get()
|
||||
|
||||
#client.publish(msg[0], msg[1])
|
||||
#apis.ha_api.call_service(service="esphome/" + self._api_panel_name + "_nspanelui_api_call", command=2, data=msg)
|
||||
service = msg[0] + "_nspanelui_api_call"
|
||||
service_data = {
|
||||
"data": msg[1],
|
||||
"command":2
|
||||
try:
|
||||
msg = panel_out_queue.get()
|
||||
service = msg[0] + "_nspanelui_api_call"
|
||||
service_data = {
|
||||
"data": msg[1],
|
||||
"command": 2
|
||||
}
|
||||
libs.home_assistant.send_msg_to_panel(
|
||||
service = service,
|
||||
service_data = service_data
|
||||
)
|
||||
libs.home_assistant.send_msg_to_panel(
|
||||
service=service,
|
||||
service_data=service_data
|
||||
)
|
||||
except Exception:
|
||||
logging.exception("Failed to process outgoing panel message")
|
||||
|
||||
|
||||
def connect():
|
||||
global settings, panel_out_queue
|
||||
ha_is_configured = settings["home_assistant_address"] != "" and settings["home_assistant_token"] != ""
|
||||
if "mqtt_server" in settings and not "use_ha_api" in settings:
|
||||
MqttManager(settings, panel_out_queue, panel_in_queues)
|
||||
else:
|
||||
logging.info("MQTT values not configured, will not connect.")
|
||||
|
||||
# MQTT Connected, start APIs if configured
|
||||
if settings["home_assistant_address"] != "" and settings["home_assistant_token"] != "":
|
||||
if ha_is_configured:
|
||||
libs.home_assistant.init(settings, on_ha_update)
|
||||
libs.home_assistant.connect()
|
||||
else:
|
||||
logging.info("Home Assistant values not configured, will not connect.")
|
||||
return
|
||||
|
||||
wait_seconds = 0
|
||||
while not libs.home_assistant.ws_connected:
|
||||
wait_seconds += 1
|
||||
if wait_seconds % 10 == 0:
|
||||
logging.info("Waiting for Home Assistant websocket connection... (%ss)", wait_seconds)
|
||||
time.sleep(1)
|
||||
if settings.get("use_ha_api"):
|
||||
libs.home_assistant.subscribe_to_nspanel_events(on_ha_panel_event)
|
||||
@@ -97,13 +120,20 @@ def setup_panels():
|
||||
panel_thread.start()
|
||||
|
||||
def panel_thread_target(queue_in, name, settings_panel, queue_out):
|
||||
panel = LovelaceUIPanel(name, settings_panel, queue_out)
|
||||
try:
|
||||
panel = LovelaceUIPanel(name, settings_panel, queue_out)
|
||||
except Exception:
|
||||
logging.exception("Failed to initialize panel thread for '%s'", name)
|
||||
return
|
||||
while True:
|
||||
msg = queue_in.get()
|
||||
if msg[0] == "MQTT:":
|
||||
panel.customrecv_event_callback(msg[1])
|
||||
elif msg[0] == "HA:":
|
||||
panel.ha_event_callback(msg[1])
|
||||
try:
|
||||
msg = queue_in.get()
|
||||
if msg[0] == "MQTT:":
|
||||
panel.customrecv_event_callback(msg[1])
|
||||
elif msg[0] == "HA:":
|
||||
panel.ha_event_callback(msg[1])
|
||||
except Exception:
|
||||
logging.exception("Panel thread '%s' failed while handling queue message", name)
|
||||
|
||||
def get_config_file():
|
||||
CONFIG_FILE = os.getenv('CONFIG_FILE')
|
||||
@@ -117,18 +147,27 @@ def get_config(file):
|
||||
try:
|
||||
with open(file, 'r', encoding="utf8") as file:
|
||||
settings = yaml.safe_load(file)
|
||||
except FileNotFoundError:
|
||||
logging.error("Config file not found: %s", file)
|
||||
return False
|
||||
except OSError:
|
||||
logging.exception("Failed reading config file: %s", file)
|
||||
return False
|
||||
except yaml.YAMLError as exc:
|
||||
print ("Error while parsing YAML file:")
|
||||
logging.error("Error while parsing YAML file: %s", file)
|
||||
if hasattr(exc, 'problem_mark'):
|
||||
if exc.context != None:
|
||||
print (' parser says\n' + str(exc.problem_mark) + '\n ' +
|
||||
str(exc.problem) + ' ' + str(exc.context) +
|
||||
'\nPlease correct data and retry.')
|
||||
logging.error(
|
||||
"Parser says\n%s\n%s %s\nPlease correct data and retry.",
|
||||
str(exc.problem_mark), str(exc.problem), str(exc.context)
|
||||
)
|
||||
else:
|
||||
print (' parser says\n' + str(exc.problem_mark) + '\n ' +
|
||||
str(exc.problem) + '\nPlease correct data and retry.')
|
||||
logging.error(
|
||||
"Parser says\n%s\n%s\nPlease correct data and retry.",
|
||||
str(exc.problem_mark), str(exc.problem)
|
||||
)
|
||||
else:
|
||||
print ("Something went wrong while parsing yaml file")
|
||||
logging.exception("Something went wrong while parsing yaml file")
|
||||
return False
|
||||
|
||||
if not settings.get("mqtt_username"):
|
||||
@@ -172,10 +211,14 @@ def config_watch():
|
||||
project_files.append(get_config_file())
|
||||
handler = ConfigChangeEventHandler(project_files)
|
||||
observer = Observer()
|
||||
observer.schedule(handler, path=os.path.dirname(get_config_file()), recursive=True)
|
||||
watch_path = os.path.dirname(get_config_file()) or "."
|
||||
observer.schedule(handler, path=watch_path, recursive=True)
|
||||
observer.start()
|
||||
while True:
|
||||
time.sleep(1)
|
||||
try:
|
||||
time.sleep(1)
|
||||
except Exception:
|
||||
logging.exception("Config watch loop failed")
|
||||
|
||||
def signal_handler(signum, frame):
|
||||
logging.info(f"Received signal {signum}. Initiating restart...")
|
||||
@@ -194,4 +237,5 @@ if __name__ == '__main__':
|
||||
time.sleep(100)
|
||||
else:
|
||||
while True:
|
||||
time.sleep(100)
|
||||
|
||||
time.sleep(100)
|
||||
|
||||
@@ -19,7 +19,6 @@ class MqttManager:
|
||||
self.client.username_pw_set(
|
||||
settings["mqtt_username"], settings["mqtt_password"])
|
||||
# Wait for connection
|
||||
connection_return_code = 0
|
||||
mqtt_server = settings["mqtt_server"]
|
||||
mqtt_port = int(settings["mqtt_port"])
|
||||
logging.info("Connecting to %s:%i as %s",
|
||||
@@ -28,9 +27,12 @@ class MqttManager:
|
||||
try:
|
||||
self.client.connect(mqtt_server, mqtt_port, 5)
|
||||
break # Connection call did not raise exception, connection is sucessfull
|
||||
except: # pylint: disable=bare-except
|
||||
except Exception: # pylint: disable=broad-exception-caught
|
||||
logging.exception(
|
||||
"Failed to connect to MQTT %s:%i. Will try again in 10 seconds. Code: %s", mqtt_server, mqtt_port, connection_return_code)
|
||||
"Failed to connect to MQTT %s:%i. Will try again in 10 seconds.",
|
||||
mqtt_server,
|
||||
mqtt_port,
|
||||
)
|
||||
time.sleep(10.)
|
||||
self.client.loop_start()
|
||||
process_thread = threading.Thread(target=self.process_in_queue, args=(self.client, self.msg_in_queue))
|
||||
@@ -38,31 +40,52 @@ class MqttManager:
|
||||
process_thread.start()
|
||||
|
||||
def on_mqtt_connect(self, client, userdata, flags, rc):
|
||||
if rc != 0:
|
||||
logging.error("MQTT connection failed with return code: %s", rc)
|
||||
return
|
||||
logging.info("Connected to MQTT Server")
|
||||
# subscribe to panelRecvTopic of each panel
|
||||
for settings_panel in self.settings["nspanels"].values():
|
||||
client.subscribe(settings_panel["panelRecvTopic"])
|
||||
topic = settings_panel["panelRecvTopic"]
|
||||
result, _ = client.subscribe(topic)
|
||||
if result == mqtt.MQTT_ERR_SUCCESS:
|
||||
logging.debug("Subscribed to panel topic: %s", topic)
|
||||
else:
|
||||
logging.error("Failed to subscribe to panel topic '%s' (result=%s)", topic, result)
|
||||
|
||||
def on_mqtt_message(self, client, userdata, msg):
|
||||
try:
|
||||
if msg.payload.decode() == "":
|
||||
payload_text = msg.payload.decode('utf-8')
|
||||
if payload_text == "":
|
||||
logging.debug("Ignoring empty MQTT payload on topic: %s", msg.topic)
|
||||
return
|
||||
if msg.topic in self.msg_out_queue_list.keys():
|
||||
data = json.loads(msg.payload.decode('utf-8'))
|
||||
data = json.loads(payload_text)
|
||||
if "CustomRecv" in data:
|
||||
queue = self.msg_out_queue_list[msg.topic]
|
||||
queue.put(("MQTT:", data["CustomRecv"]))
|
||||
else:
|
||||
logging.debug("JSON payload on topic '%s' has no 'CustomRecv' key", msg.topic)
|
||||
else:
|
||||
logging.debug("Received unhandled message on topic: %s", msg.topic)
|
||||
except UnicodeDecodeError:
|
||||
logging.exception("Failed to decode MQTT payload as UTF-8 on topic: %s", msg.topic)
|
||||
except json.JSONDecodeError:
|
||||
logging.exception("Failed to parse MQTT JSON payload on topic: %s", msg.topic)
|
||||
except Exception: # pylint: disable=broad-exception-caught
|
||||
logging.exception("Something went wrong during processing of message:")
|
||||
logging.exception("Unexpected error while processing MQTT message on topic: %s", msg.topic)
|
||||
try:
|
||||
logging.error(msg.payload.decode('utf-8'))
|
||||
except: # pylint: disable=bare-except
|
||||
except Exception: # pylint: disable=broad-exception-caught
|
||||
logging.error(
|
||||
"Something went wrong when processing the exception message, couldn't decode payload to utf-8.")
|
||||
|
||||
def process_in_queue(self, client, msg_in_queue):
|
||||
while True:
|
||||
msg = msg_in_queue.get()
|
||||
client.publish(msg[0], msg[1])
|
||||
try:
|
||||
msg = msg_in_queue.get()
|
||||
result = client.publish(msg[0], msg[1])
|
||||
if result.rc != mqtt.MQTT_ERR_SUCCESS:
|
||||
logging.error("Failed publishing message to topic '%s' (rc=%s)", msg[0], result.rc)
|
||||
except Exception: # pylint: disable=broad-exception-caught
|
||||
logging.exception("Failed processing outgoing MQTT queue message")
|
||||
|
||||
@@ -103,10 +103,13 @@ class LovelaceUIPanel:
|
||||
libs.panel_cmd.page_type(self.msg_out_queue, self.sendTopic, "pageStartup")
|
||||
|
||||
|
||||
def schedule_thread_target(self):
|
||||
while True:
|
||||
self.schedule.exec_jobs()
|
||||
time.sleep(1)
|
||||
def schedule_thread_target(self):
|
||||
while True:
|
||||
try:
|
||||
self.schedule.exec_jobs()
|
||||
except Exception:
|
||||
logging.exception("Scheduler execution failed for panel '%s'", self.name)
|
||||
time.sleep(1)
|
||||
|
||||
def update_time(self):
|
||||
use_timezone = tz.gettz(self.settings["timeZone"])
|
||||
@@ -200,12 +203,15 @@ class LovelaceUIPanel:
|
||||
return card
|
||||
return list(self.cards.values())[0]
|
||||
|
||||
def customrecv_event_callback(self, msg):
|
||||
logging.debug("Recv Message from NsPanel (%s): %s", self.name, msg)
|
||||
msg = msg.split(",")
|
||||
# run action based on received command
|
||||
if msg[0] == "event":
|
||||
if msg[1] == "startup":
|
||||
def customrecv_event_callback(self, msg):
|
||||
logging.debug("Recv Message from NsPanel (%s): %s", self.name, msg)
|
||||
msg = msg.split(",")
|
||||
if len(msg) < 2:
|
||||
logging.error("Malformed panel message on '%s': %s", self.name, msg)
|
||||
return
|
||||
# run action based on received command
|
||||
if msg[0] == "event":
|
||||
if msg[1] == "startup":
|
||||
# TODO: Handle Update Messages
|
||||
self.update_date()
|
||||
self.update_time()
|
||||
@@ -226,15 +232,24 @@ class LovelaceUIPanel:
|
||||
self.render_current_page(switchPages=True)
|
||||
if msg[1] == "renderCurrentPage":
|
||||
self.render_current_page(requested=True)
|
||||
if msg[1] == "buttonPress2":
|
||||
entity_id = msg[2]
|
||||
if entity_id == "":
|
||||
return
|
||||
btype = msg[3]
|
||||
value = msg[4] if len(msg) > 4 else None
|
||||
if btype == "bExit":
|
||||
if entity_id in ["screensaver", "screensaver2"] and self.settings.get("screensaver").get("doubleTapToUnlock") and value == "1":
|
||||
return
|
||||
if msg[1] == "buttonPress2":
|
||||
if len(msg) < 4:
|
||||
logging.error("Malformed buttonPress2 payload on '%s': %s", self.name, msg)
|
||||
return
|
||||
entity_id = msg[2]
|
||||
if entity_id == "":
|
||||
return
|
||||
btype = msg[3]
|
||||
value = msg[4] if len(msg) > 4 else None
|
||||
entity_config = {}
|
||||
action_context = {
|
||||
"panel": self.name,
|
||||
"btype": btype,
|
||||
"value": value,
|
||||
}
|
||||
if btype == "bExit":
|
||||
if entity_id in ["screensaver", "screensaver2"] and self.settings.get("screensaver").get("doubleTapToUnlock") and value == "1":
|
||||
return
|
||||
|
||||
# in case privious_cards is empty add a default card
|
||||
if len(self.privious_cards) == 0:
|
||||
@@ -249,12 +264,12 @@ class LovelaceUIPanel:
|
||||
return
|
||||
|
||||
# replace iid with real entity id
|
||||
if entity_id.startswith("iid."):
|
||||
iid = entity_id.split(".")[1]
|
||||
for e in self.current_card.entities:
|
||||
if e.iid == iid:
|
||||
entity_id = e.entity_id
|
||||
entity_config = e.config
|
||||
if entity_id.startswith("iid."):
|
||||
iid = entity_id.split(".")[1]
|
||||
for e in self.current_card.entities:
|
||||
if e.iid == iid:
|
||||
entity_id = e.entity_id
|
||||
entity_config = e.config
|
||||
|
||||
match btype:
|
||||
case 'button':
|
||||
@@ -271,31 +286,51 @@ class LovelaceUIPanel:
|
||||
self.privious_cards.append(self.current_card)
|
||||
self.current_card = self.searchCard(card_iid)
|
||||
self.render_current_page(switchPages=True)
|
||||
# send ha stuff to ha
|
||||
case _:
|
||||
ha_control.handle_buttons(entity_id, btype, value, entity_config=entity_config)
|
||||
# send ha stuff to ha
|
||||
case _:
|
||||
ha_control.handle_buttons(
|
||||
entity_id,
|
||||
btype,
|
||||
value,
|
||||
entity_config=entity_config,
|
||||
action_context=action_context,
|
||||
)
|
||||
case 'cardUnlock-unlock':
|
||||
card_iid = entity_id.split(".")[1]
|
||||
if int(self.current_card.config.get("pin")) == int(value):
|
||||
self.privious_cards.append(self.current_card)
|
||||
self.current_card = self.searchCard(card_iid)
|
||||
self.render_current_page(switchPages=True)
|
||||
case 'mode-light':
|
||||
ha_control.handle_buttons(entity_id, btype, value, entity_config=entity_config)
|
||||
case _:
|
||||
ha_control.handle_buttons(entity_id, btype, value)
|
||||
case 'mode-light':
|
||||
ha_control.handle_buttons(
|
||||
entity_id,
|
||||
btype,
|
||||
value,
|
||||
entity_config=entity_config,
|
||||
action_context=action_context,
|
||||
)
|
||||
case _:
|
||||
ha_control.handle_buttons(
|
||||
entity_id,
|
||||
btype,
|
||||
value,
|
||||
action_context=action_context,
|
||||
)
|
||||
|
||||
if msg[1] == "pageOpenDetail":
|
||||
entity_id = msg[3]
|
||||
# replace iid with real entity id
|
||||
if entity_id.startswith("iid."):
|
||||
iid = entity_id.split(".")[1]
|
||||
for e in self.current_card.entities:
|
||||
if e.iid == iid:
|
||||
entity_id = e.entity_id
|
||||
effectList = None
|
||||
if entity_id.startswith("light"):
|
||||
effectList = e.config.get("effectList")
|
||||
if msg[1] == "pageOpenDetail":
|
||||
if len(msg) < 4:
|
||||
logging.error("Malformed pageOpenDetail payload on '%s': %s", self.name, msg)
|
||||
return
|
||||
entity_id = msg[3]
|
||||
effectList = None
|
||||
# replace iid with real entity id
|
||||
if entity_id.startswith("iid."):
|
||||
iid = entity_id.split(".")[1]
|
||||
for e in self.current_card.entities:
|
||||
if e.iid == iid:
|
||||
entity_id = e.entity_id
|
||||
if entity_id.startswith("light"):
|
||||
effectList = e.config.get("effectList")
|
||||
if msg[2] == "popupInSel": #entity_id.split(".")[0] in ['input_select', 'media_player']:
|
||||
libs.panel_cmd.entityUpdateDetail2(self.msg_out_queue, self.sendTopic, detail_open(self.settings["locale"], msg[2], entity_id, msg[3], self.msg_out_queue, sendTopic=self.sendTopic, options_list=effectList))
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user