diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..3465ced --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,40 @@ +name: Build Kodi Addon + +on: + push: + branches: [master] + tags: ["v*"] + pull_request: + branches: [master] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Extract version from addon.xml + id: version + run: | + version=$(grep -oP 'version="\K[^"]+' service.video.hdrnotify/addon.xml | head -1) + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "Addon version: $version" + + - name: Build ZIP package + run: | + zip -r "service.video.hdrnotify-${{ steps.version.outputs.version }}.zip" \ + service.video.hdrnotify/ \ + -x "service.video.hdrnotify/.*" + + - name: Upload artifact + uses: actions/upload-artifact@v3 + with: + name: service.video.hdrnotify-${{ steps.version.outputs.version }} + path: service.video.hdrnotify-${{ steps.version.outputs.version }}.zip + + - name: Create Gitea Release + if: startsWith(github.ref, 'refs/tags/v') + uses: https://gitea.com/actions/gitea-release-action@v1 + with: + files: service.video.hdrnotify-${{ steps.version.outputs.version }}.zip + token: '${{ secrets.GITHUB_TOKEN }}' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..ed1c987 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,42 @@ +name: Build Kodi Addon + +on: + push: + branches: [master] + tags: ["v*"] + pull_request: + branches: [master] + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v6 + + - name: Extract version from addon.xml + id: version + run: | + version=$(grep -oP 'version="\K[^"]+' service.video.hdrnotify/addon.xml | head -1) + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "Addon version: $version" + + - name: Build ZIP package + run: | + zip -r "service.video.hdrnotify-${{ steps.version.outputs.version }}.zip" \ + service.video.hdrnotify/ \ + -x "service.video.hdrnotify/.*" + + - name: Upload artifact + uses: actions/upload-artifact@v7 + with: + name: service.video.hdrnotify-${{ steps.version.outputs.version }} + path: service.video.hdrnotify-${{ steps.version.outputs.version }}.zip + + - name: Create GitHub Release + if: startsWith(github.ref, 'refs/tags/v') + uses: softprops/action-gh-release@v3 + with: + files: service.video.hdrnotify-${{ steps.version.outputs.version }}.zip + generate_release_notes: true diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..aedaf0e --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Oleksandr Berezovskyi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index e69de29..b22ae5e 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,114 @@ +# HDR Notify for Kodi + +A Kodi service addon that detects the HDR/SDR state of the currently playing video and sends it to external services -- so they can react automatically without relying on signal auto-detection. + +## The Problem + +[HyperHDR](https://github.com/awawa-dev/HyperHDR) provides ambient lighting with HDR tone mapping support, but it needs to know *when* to apply tone mapping. Automatic detection is unreliable on some setups, causing washed-out colors or incorrect tone mapping. Similarly, you may want [Home Assistant](https://www.home-assistant.io/) to react to HDR content -- for example, adjusting room lighting or triggering scenes. + +## How It Works + +``` +Kodi (playing video) + | + | onAVStarted / onPlayBackStopped / onPlayBackEnded + | + v +HDR Notify (this addon) + | + | reads VideoPlayer.HdrType + | + |--- HyperHDR: POST /json-rpc {"command":"videomodehdr", "HDR": 1|0} + | + '--- Home Assistant: POST /api/webhook/ {"hdr": true|false, "hdr_type": "..."} +``` + +1. When video playback starts, the addon reads the [`VideoPlayer.HdrType`](https://kodi.wiki/view/InfoLabels) InfoLabel +2. Any non-empty value (e.g. `hdr10`, `dolbyvision`, `hlg`) means HDR is active +3. The addon sends the state to whichever targets are enabled +4. When playback stops or ends, it sends SDR (reset) to all enabled targets + +Both targets are independent -- you can enable one, both, or neither. + +## Compatibility + +- **Kodi 20 (Nexus)** and later -- the `VideoPlayer.HdrType` InfoLabel was introduced in Kodi 20 +- No external dependencies -- uses Python stdlib only + +## Installation + +1. Go to the [Releases](../../releases) page +2. Download the latest `service.video.hdrnotify-*.zip` +3. In Kodi: **Settings > Add-ons > Install from zip file** > select the downloaded ZIP +4. Configure the addon via **Settings > Add-ons > My add-ons > Services > HDR Notify** + +## Configuration + +The addon settings are split into two tabs: + +### HyperHDR + +| Setting | Default | Description | +|---------|---------|-------------| +| Enable HyperHDR | On | Toggle HyperHDR notifications | +| Host | `127.0.0.1` | HyperHDR IP or hostname | +| Port | `8090` | JSON API port (default HTTPS port: `8443`) | +| Use HTTPS | Off | Connect via HTTPS | +| API Token | *(empty)* | Auth token, if [token-based authentication](https://github.com/awawa-dev/HyperHDR/wiki) is enabled | + +The addon sends the [`videomodehdr`](https://github.com/awawa-dev/HyperHDR) command to the HyperHDR JSON API at `/json-rpc`. + +### Home Assistant + +| Setting | Default | Description | +|---------|---------|-------------| +| Enable Home Assistant | Off | Toggle HA webhook notifications | +| Host | `127.0.0.1` | Home Assistant IP or hostname | +| Port | `8123` | HA port | +| Use HTTPS | Off | Connect via HTTPS | +| Webhook ID | *(empty)* | The webhook ID from your HA automation trigger | + +The addon sends a JSON payload to the HA [webhook trigger](https://www.home-assistant.io/docs/automation/trigger/#webhook-trigger) endpoint: + +```json +{"hdr": true, "hdr_type": "hdr10"} +``` + +No authentication token is needed -- HA webhooks are auth-free by design. + +#### Example Home Assistant Automation + +```yaml +automation: + - alias: "React to Kodi HDR state" + trigger: + - platform: webhook + webhook_id: "kodi_hdr_state" + allowed_methods: + - POST + local_only: true + action: + - choose: + - conditions: + - condition: template + value_template: "{{ trigger.json.hdr }}" + sequence: + - service: light.turn_off + target: + entity_id: light.living_room + default: + - service: light.turn_on + target: + entity_id: light.living_room +``` + +## Related Projects + +- [HyperHDR](https://github.com/awawa-dev/HyperHDR) -- Open-source ambient lighting with HDR tone mapping +- [Home Assistant](https://www.home-assistant.io/) -- Open-source home automation platform +- [Kodi](https://kodi.tv/) -- Open-source media center +- [CoreELEC](https://coreelec.org/) -- Lightweight Kodi-focused OS for Amlogic devices + +## License + +[MIT](LICENSE.md) diff --git a/service.video.hdrnotify/addon.xml b/service.video.hdrnotify/addon.xml new file mode 100644 index 0000000..fc2cdf7 --- /dev/null +++ b/service.video.hdrnotify/addon.xml @@ -0,0 +1,18 @@ + + + + + + + + Notifies HyperHDR and Home Assistant about HDR/SDR playback state + Сповіщає HyperHDR та Home Assistant про стан HDR/SDR відтворення + Automatically detects HDR/SDR mode of currently playing video and sends the state to HyperHDR (to toggle tone mapping) and/or Home Assistant (via webhook). Supports Kodi 20 (Nexus) and later. + Автоматично визначає режим HDR/SDR поточного відео та надсилає стан до HyperHDR (для перемикання тонального відображення) та/або Home Assistant (через вебхук). Підтримує Kodi 20 (Nexus) та новіші версії. + all + MIT + + diff --git a/service.video.hdrnotify/resources/language/resource.language.en_gb/strings.po b/service.video.hdrnotify/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000..5c0de46 --- /dev/null +++ b/service.video.hdrnotify/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,106 @@ +# Kodi Media Center language file +# Addon Name: HDR Notify +# Addon id: service.video.hdrnotify +msgid "" +msgstr "" +"Project-Id-Version: service.video.hdrnotify\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2026-03-21 00:00+0000\n" +"Last-Translator: \n" +"Language-Team: English\n" +"Language: en\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +# Category labels +msgctxt "#32001" +msgid "HyperHDR" +msgstr "" + +msgctxt "#32002" +msgid "Home Assistant" +msgstr "" + +# HyperHDR settings +msgctxt "#32010" +msgid "Enable HyperHDR" +msgstr "" + +msgctxt "#32011" +msgid "Send HDR/SDR state to HyperHDR when playback starts or stops" +msgstr "" + +msgctxt "#32012" +msgid "HyperHDR host" +msgstr "" + +msgctxt "#32013" +msgid "IP address or hostname of your HyperHDR instance" +msgstr "" + +msgctxt "#32014" +msgid "HyperHDR port" +msgstr "" + +msgctxt "#32015" +msgid "Port number for HyperHDR JSON API" +msgstr "" + +msgctxt "#32016" +msgid "Use HTTPS" +msgstr "" + +msgctxt "#32017" +msgid "Connect to HyperHDR using HTTPS (default port: 8443)" +msgstr "" + +msgctxt "#32018" +msgid "API token" +msgstr "" + +msgctxt "#32019" +msgid "Authentication token (leave empty if authentication is disabled)" +msgstr "" + +# Home Assistant settings +msgctxt "#32030" +msgid "Enable Home Assistant" +msgstr "" + +msgctxt "#32031" +msgid "Send HDR/SDR state to Home Assistant via webhook" +msgstr "" + +msgctxt "#32032" +msgid "Home Assistant host" +msgstr "" + +msgctxt "#32033" +msgid "IP address or hostname of your Home Assistant instance" +msgstr "" + +msgctxt "#32034" +msgid "Home Assistant port" +msgstr "" + +msgctxt "#32035" +msgid "Port number for Home Assistant" +msgstr "" + +msgctxt "#32036" +msgid "Use HTTPS" +msgstr "" + +msgctxt "#32037" +msgid "Connect to Home Assistant using HTTPS" +msgstr "" + +msgctxt "#32038" +msgid "Webhook ID" +msgstr "" + +msgctxt "#32039" +msgid "Webhook ID from your Home Assistant automation trigger" +msgstr "" diff --git a/service.video.hdrnotify/resources/language/resource.language.uk_ua/strings.po b/service.video.hdrnotify/resources/language/resource.language.uk_ua/strings.po new file mode 100644 index 0000000..fe8b96d --- /dev/null +++ b/service.video.hdrnotify/resources/language/resource.language.uk_ua/strings.po @@ -0,0 +1,106 @@ +# Kodi Media Center language file +# Addon Name: HDR Notify +# Addon id: service.video.hdrnotify +msgid "" +msgstr "" +"Project-Id-Version: service.video.hdrnotify\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2026-03-21 00:00+0000\n" +"Last-Translator: \n" +"Language-Team: Ukrainian\n" +"Language: uk\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +# Category labels +msgctxt "#32001" +msgid "HyperHDR" +msgstr "HyperHDR" + +msgctxt "#32002" +msgid "Home Assistant" +msgstr "Home Assistant" + +# HyperHDR settings +msgctxt "#32010" +msgid "Enable HyperHDR" +msgstr "Увімкнути HyperHDR" + +msgctxt "#32011" +msgid "Send HDR/SDR state to HyperHDR when playback starts or stops" +msgstr "Надсилати стан HDR/SDR до HyperHDR при початку або зупинці відтворення" + +msgctxt "#32012" +msgid "HyperHDR host" +msgstr "Хост HyperHDR" + +msgctxt "#32013" +msgid "IP address or hostname of your HyperHDR instance" +msgstr "IP-адреса або ім'я хоста вашого HyperHDR" + +msgctxt "#32014" +msgid "HyperHDR port" +msgstr "Порт HyperHDR" + +msgctxt "#32015" +msgid "Port number for HyperHDR JSON API" +msgstr "Номер порту для JSON API HyperHDR" + +msgctxt "#32016" +msgid "Use HTTPS" +msgstr "Використовувати HTTPS" + +msgctxt "#32017" +msgid "Connect to HyperHDR using HTTPS (default port: 8443)" +msgstr "Підключатися до HyperHDR через HTTPS (порт за замовчуванням: 8443)" + +msgctxt "#32018" +msgid "API token" +msgstr "Токен API" + +msgctxt "#32019" +msgid "Authentication token (leave empty if authentication is disabled)" +msgstr "Токен автентифікації (залиште порожнім, якщо автентифікація вимкнена)" + +# Home Assistant settings +msgctxt "#32030" +msgid "Enable Home Assistant" +msgstr "Увімкнути Home Assistant" + +msgctxt "#32031" +msgid "Send HDR/SDR state to Home Assistant via webhook" +msgstr "Надсилати стан HDR/SDR до Home Assistant через вебхук" + +msgctxt "#32032" +msgid "Home Assistant host" +msgstr "Хост Home Assistant" + +msgctxt "#32033" +msgid "IP address or hostname of your Home Assistant instance" +msgstr "IP-адреса або ім'я хоста вашого Home Assistant" + +msgctxt "#32034" +msgid "Home Assistant port" +msgstr "Порт Home Assistant" + +msgctxt "#32035" +msgid "Port number for Home Assistant" +msgstr "Номер порту Home Assistant" + +msgctxt "#32036" +msgid "Use HTTPS" +msgstr "Використовувати HTTPS" + +msgctxt "#32037" +msgid "Connect to Home Assistant using HTTPS" +msgstr "Підключатися до Home Assistant через HTTPS" + +msgctxt "#32038" +msgid "Webhook ID" +msgstr "Ідентифікатор вебхука" + +msgctxt "#32039" +msgid "Webhook ID from your Home Assistant automation trigger" +msgstr "Ідентифікатор вебхука з тригера автоматизації Home Assistant" diff --git a/service.video.hdrnotify/resources/settings.xml b/service.video.hdrnotify/resources/settings.xml new file mode 100644 index 0000000..05db77d --- /dev/null +++ b/service.video.hdrnotify/resources/settings.xml @@ -0,0 +1,109 @@ + + +
+ + + + 0 + true + + + + 0 + 127.0.0.1 + + true + + + false + + + + + 0 + 8090 + + true + + + 1 + 65535 + + + + + 0 + false + + true + + + + + 0 + + + true + + + true + + + true + + + + + + + + 0 + false + + + + 0 + 127.0.0.1 + + true + + + false + + + + + 0 + 8123 + + true + + + 1 + 65535 + + + + + 0 + false + + true + + + + + 0 + + + true + + + true + + + + + +
+
diff --git a/service.video.hdrnotify/service.py b/service.video.hdrnotify/service.py new file mode 100644 index 0000000..48baf9e --- /dev/null +++ b/service.video.hdrnotify/service.py @@ -0,0 +1,134 @@ +import json +import ssl +import urllib.request + +import xbmc +import xbmcaddon + +ADDON_ID = "service.video.hdrnotify" +LOG_PREFIX = "[HDR Notify]" + + +def log(msg, level=xbmc.LOGINFO): + xbmc.log(f"{LOG_PREFIX} {msg}", level) + + +def read_settings(): + addon = xbmcaddon.Addon(ADDON_ID) + return { + "hyperhdr_enabled": addon.getSettingBool("hyperhdr_enabled"), + "hyperhdr_host": addon.getSetting("hyperhdr_host"), + "hyperhdr_port": addon.getSettingInt("hyperhdr_port"), + "hyperhdr_ssl": addon.getSettingBool("hyperhdr_ssl"), + "hyperhdr_token": addon.getSetting("hyperhdr_token"), + "ha_enabled": addon.getSettingBool("ha_enabled"), + "ha_host": addon.getSetting("ha_host"), + "ha_port": addon.getSettingInt("ha_port"), + "ha_ssl": addon.getSettingBool("ha_ssl"), + "ha_webhook_id": addon.getSetting("ha_webhook_id"), + } + + +def _post_json(url, data, use_ssl=False): + payload = json.dumps(data).encode("utf-8") + req = urllib.request.Request( + url, data=payload, headers={"Content-Type": "application/json"} + ) + ctx = None + if use_ssl: + ctx = ssl.create_default_context() + urllib.request.urlopen(req, timeout=2, context=ctx) + + +def notify_hyperhdr(settings, is_hdr): + host = settings["hyperhdr_host"] + port = settings["hyperhdr_port"] + if not host: + return + scheme = "https" if settings["hyperhdr_ssl"] else "http" + url = f"{scheme}://{host}:{port}/json-rpc" + use_ssl = settings["hyperhdr_ssl"] + try: + token = settings["hyperhdr_token"] + if token: + _post_json( + url, + {"command": "authorize", "subcommand": "login", "token": token}, + use_ssl, + ) + _post_json( + url, + {"command": "videomodehdr", "tan": 1, "HDR": 1 if is_hdr else 0}, + use_ssl, + ) + log(f"HyperHDR notified: HDR={'on' if is_hdr else 'off'}") + except Exception as e: + log(f"HyperHDR error: {e}", xbmc.LOGWARNING) + + +def notify_homeassistant(settings, is_hdr, hdr_type): + host = settings["ha_host"] + webhook_id = settings["ha_webhook_id"] + if not host or not webhook_id: + return + port = settings["ha_port"] + scheme = "https" if settings["ha_ssl"] else "http" + url = f"{scheme}://{host}:{port}/api/webhook/{webhook_id}" + try: + _post_json( + url, + {"hdr": is_hdr, "hdr_type": hdr_type}, + settings["ha_ssl"], + ) + log(f"Home Assistant notified: hdr={is_hdr}, hdr_type='{hdr_type}'") + except Exception as e: + log(f"Home Assistant error: {e}", xbmc.LOGWARNING) + + +def notify_targets(settings, is_hdr, hdr_type=""): + if settings["hyperhdr_enabled"]: + notify_hyperhdr(settings, is_hdr) + if settings["ha_enabled"]: + notify_homeassistant(settings, is_hdr, hdr_type) + + +class HDRMonitor(xbmc.Monitor): + def __init__(self): + super().__init__() + self.settings = read_settings() + + def onSettingsChanged(self): + self.settings = read_settings() + log("Settings reloaded") + + +class HDRPlayer(xbmc.Player): + def __init__(self, monitor): + super().__init__() + self._monitor = monitor + + def onAVStarted(self): + hdr_type = xbmc.getInfoLabel("VideoPlayer.HdrType") + is_hdr = bool(hdr_type) + log(f"Playback started: hdr_type='{hdr_type}', is_hdr={is_hdr}") + notify_targets(self._monitor.settings, is_hdr, hdr_type) + + def onPlayBackStopped(self): + log("Playback stopped") + notify_targets(self._monitor.settings, False) + + def onPlayBackEnded(self): + log("Playback ended") + notify_targets(self._monitor.settings, False) + + +if __name__ == "__main__": + log("Service started") + monitor = HDRMonitor() + player = HDRPlayer(monitor) + + while not monitor.abortRequested(): + if monitor.waitForAbort(1): + break + + log("Service stopped")