updates2mqtt 1.3.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
updates2mqtt/mqtt.py ADDED
@@ -0,0 +1,286 @@
1
+ import asyncio
2
+ import json
3
+ import time
4
+ from collections.abc import Callable
5
+ from dataclasses import dataclass, field
6
+ from typing import Any
7
+
8
+ import paho.mqtt.client as mqtt
9
+ import paho.mqtt.subscribeoptions
10
+ import structlog
11
+ from paho.mqtt.client import MQTTMessage
12
+ from paho.mqtt.enums import CallbackAPIVersion
13
+ from paho.mqtt.properties import Properties
14
+ from paho.mqtt.reasoncodes import ReasonCode
15
+
16
+ from updates2mqtt.model import Discovery, ReleaseProvider
17
+
18
+ from .config import HomeAssistantConfig, MqttConfig, NodeConfig
19
+ from .hass_formatter import hass_format_config, hass_format_state
20
+
21
+ log = structlog.get_logger()
22
+
23
+
24
+ @dataclass
25
+ class LocalMessage:
26
+ topic: str | None = field(default=None)
27
+ payload: str | None = field(default=None)
28
+
29
+
30
+ class MqttClient:
31
+ def __init__(self, cfg: MqttConfig, node_cfg: NodeConfig, hass_cfg: HomeAssistantConfig) -> None:
32
+ self.cfg: MqttConfig = cfg
33
+ self.node_cfg: NodeConfig = node_cfg
34
+ self.hass_cfg: HomeAssistantConfig = hass_cfg
35
+ self.providers_by_topic: dict[str, ReleaseProvider] = {}
36
+ self.event_loop: asyncio.AbstractEventLoop | None = None
37
+ self.client: mqtt.Client | None = None
38
+ self.log = structlog.get_logger().bind(host=cfg.host, integration="mqtt")
39
+
40
+ def start(self, event_loop: asyncio.AbstractEventLoop | None = None) -> None:
41
+ logger = self.log.bind(action="start")
42
+ try:
43
+ self.event_loop = event_loop or asyncio.get_event_loop()
44
+ self.client = mqtt.Client(
45
+ callback_api_version=CallbackAPIVersion.VERSION2,
46
+ client_id=f"updates2mqtt_{self.node_cfg.name}",
47
+ clean_session=True,
48
+ )
49
+ self.client.username_pw_set(self.cfg.user, password=self.cfg.password)
50
+ self.client.connect(host=self.cfg.host, port=self.cfg.port, keepalive=60)
51
+
52
+ self.client.on_connect = self.on_connect
53
+ self.client.on_disconnect = self.on_disconnect
54
+ self.client.on_message = self.on_message
55
+
56
+ self.client.loop_start()
57
+
58
+ logger.info("Connected to broker", host=self.cfg.host, port=self.cfg.port)
59
+ except Exception as e:
60
+ logger.error("Failed to connect to broker", host=self.cfg.host, port=self.cfg.port, error=str(e))
61
+ raise OSError(f"Connection Failure to {self.cfg.host}:{self.cfg.port} as {self.cfg.user} -- {e}") from e
62
+
63
+ def stop(self) -> None:
64
+ if self.client:
65
+ self.client.loop_stop()
66
+ self.client.disconnect()
67
+ self.client = None
68
+
69
+ def on_connect(
70
+ self, _client: mqtt.Client, _userdata: Any, _flags: mqtt.ConnectFlags, rc: ReasonCode, _props: Properties | None
71
+ ) -> None:
72
+ if not self.client:
73
+ self.log.warn("No client, check if started")
74
+ return
75
+ self.log.info("Connected to broker", result_code=rc)
76
+ for topic in self.providers_by_topic:
77
+ self.log.info("(Re)subscribing", topic=topic)
78
+ self.client.subscribe(topic)
79
+
80
+ def on_disconnect(
81
+ self,
82
+ _client: mqtt.Client,
83
+ _userdata: Any,
84
+ _disconnect_flags: mqtt.DisconnectFlags,
85
+ rc: ReasonCode,
86
+ _props: Properties | None,
87
+ ) -> None:
88
+ self.log.info("Disconnected from broker", result_code=rc)
89
+
90
+ async def clean_topics(
91
+ self, provider: ReleaseProvider, last_scan_session: str | None, wait_time: int = 5, force: bool = False
92
+ ) -> None:
93
+ logger = self.log.bind(action="clean")
94
+ logger.info("Starting clean cycle")
95
+ cleaner = mqtt.Client(
96
+ callback_api_version=CallbackAPIVersion.VERSION1,
97
+ client_id=f"updates2mqtt_clean_{self.node_cfg.name}",
98
+ clean_session=True,
99
+ )
100
+ results = {"cleaned": 0, "handled": 0, "discovered": 0, "last_timestamp": time.time()}
101
+ cleaner.username_pw_set(self.cfg.user, password=self.cfg.password)
102
+ cleaner.connect(host=self.cfg.host, port=self.cfg.port, keepalive=60)
103
+ prefixes = [
104
+ f"{self.hass_cfg.discovery.prefix}/update/{self.node_cfg.name}_{provider.source_type}_",
105
+ f"{self.cfg.topic_root}/{self.node_cfg.name}/{provider.source_type}/",
106
+ ]
107
+
108
+ def cleanup(_client: mqtt.Client, _userdata: Any, msg: mqtt.MQTTMessage) -> None:
109
+ if msg.retain and any(msg.topic.startswith(prefix) for prefix in prefixes):
110
+ session = None
111
+ results["discovered"] += 1
112
+ try:
113
+ payload = self.safe_json_decode(msg.payload)
114
+ session = payload.get("source_session")
115
+ except Exception as e:
116
+ log.warn(
117
+ "Unable to handle payload for %s: %s",
118
+ msg.topic,
119
+ e,
120
+ exc_info=1,
121
+ )
122
+ results["handled"] += 1
123
+ results["last_timestamp"] = time.time()
124
+ if session is not None and last_scan_session is not None and session != last_scan_session:
125
+ log.debug("Removing stale msg", topic=msg.topic, session=session)
126
+ cleaner.publish(msg.topic, "", retain=True)
127
+ results["cleaned"] += 1
128
+ elif session is None and force:
129
+ log.debug("Removing untrackable msg", topic=msg.topic)
130
+ cleaner.publish(msg.topic, "", retain=True)
131
+ results["cleaned"] += 1
132
+ else:
133
+ log.debug(
134
+ "Retaining topic with current session: %s",
135
+ msg.topic,
136
+ )
137
+ else:
138
+ log.debug("Skipping clean of %s", msg.topic)
139
+
140
+ cleaner.on_message = cleanup
141
+ options = paho.mqtt.subscribeoptions.SubscribeOptions(noLocal=True)
142
+ cleaner.subscribe(f"{self.hass_cfg.discovery.prefix}/update/#", options=options)
143
+ cleaner.subscribe(f"{self.cfg.topic_root}/{self.node_cfg.name}/{provider.source_type}/#", options=options)
144
+
145
+ while time.time() - results["last_timestamp"] <= wait_time:
146
+ cleaner.loop(0.5)
147
+
148
+ log.info(
149
+ f"Clean completed, discovered:{results['discovered']}, handled:{results['handled']}, cleaned:{results['cleaned']}"
150
+ )
151
+
152
+ def safe_json_decode(self, jsonish: str | bytes | None) -> dict:
153
+ if jsonish is None:
154
+ return {}
155
+ try:
156
+ return json.loads(jsonish)
157
+ except Exception:
158
+ log.exception("JSON decode fail (%s); %s", jsonish)
159
+ try:
160
+ return json.loads(jsonish[1:-1])
161
+ except Exception:
162
+ log.exception("JSON decode fail (%s): %s", jsonish[1:-1])
163
+ return {}
164
+
165
+ async def execute_command(
166
+ self, msg: MQTTMessage | LocalMessage, on_update_start: Callable, on_update_end: Callable
167
+ ) -> None:
168
+ logger = self.log.bind(topic=msg.topic, payload=msg.payload)
169
+ comp_name: str | None = None
170
+ command: str | None = None
171
+ try:
172
+ logger.info("Execution starting")
173
+ source_type: str | None = None
174
+
175
+ payload: str | None = None
176
+ if isinstance(msg.payload, bytes):
177
+ payload = msg.payload.decode("utf-8")
178
+ elif isinstance(msg.payload, str):
179
+ payload = msg.payload
180
+ if payload and "|" in payload:
181
+ source_type, comp_name, command = payload.split("|")
182
+
183
+ provider: ReleaseProvider | None = self.providers_by_topic.get(msg.topic) if msg.topic else None
184
+ if not provider:
185
+ logger.warn("Unexpected provider type %s", msg.topic)
186
+ elif provider.source_type != source_type:
187
+ logger.warn("Unexpected source type %s", source_type)
188
+ elif command != "install" or not comp_name:
189
+ logger.warn("Invalid payload in command message: %s", msg.payload)
190
+ else:
191
+ logger.info(
192
+ "Passing %s command to %s scanner for %s",
193
+ command,
194
+ source_type,
195
+ comp_name,
196
+ )
197
+ updated = provider.command(comp_name, command, on_update_start, on_update_end)
198
+ discovery = provider.resolve(comp_name)
199
+ if updated and discovery:
200
+ self.publish_hass_state(discovery, updated)
201
+ else:
202
+ logger.debug("No change to republish after execution")
203
+ logger.info("Execution ended")
204
+ except Exception:
205
+ logger.exception("Execution failed", component=comp_name, command=command)
206
+
207
+ def local_message(self, discovery: Discovery, command: str) -> None:
208
+ """Simulate an incoming MQTT message for local commands"""
209
+ msg = LocalMessage(
210
+ topic=self.command_topic(discovery.provider), payload="|".join([discovery.source_type, discovery.name, command])
211
+ )
212
+ self.handle_message(msg)
213
+
214
+ def on_message(self, _client: mqtt.Client, _userdata: Any, msg: mqtt.MQTTMessage) -> None:
215
+ """Callback for incoming MQTT messages""" # noqa: D401
216
+ if msg.topic in self.providers_by_topic:
217
+ self.handle_message(msg)
218
+ else:
219
+ self.log.warn("Unhandled message: %s", msg.topic)
220
+
221
+ def handle_message(self, msg: mqtt.MQTTMessage | LocalMessage) -> None:
222
+ def update_start(discovery: Discovery) -> None:
223
+ self.publish_hass_state(discovery, in_progress=True)
224
+
225
+ def update_end(discovery: Discovery) -> None:
226
+ self.publish_hass_state(discovery, in_progress=False)
227
+
228
+ if self.event_loop is not None:
229
+ asyncio.run_coroutine_threadsafe(self.execute_command(msg, update_start, update_end), self.event_loop)
230
+ else:
231
+ self.log.error("No event loop to handle message", topic=msg.topic)
232
+
233
+ def config_topic(self, discovery: Discovery) -> str:
234
+ prefix = self.hass_cfg.discovery.prefix
235
+ return f"{prefix}/update/{self.node_cfg.name}_{discovery.source_type}_{discovery.name}/update/config"
236
+
237
+ def state_topic(self, discovery: Discovery) -> str:
238
+ return f"{self.cfg.topic_root}/{self.node_cfg.name}/{discovery.source_type}/{discovery.name}"
239
+
240
+ def command_topic(self, provider: ReleaseProvider) -> str:
241
+ return f"{self.cfg.topic_root}/{self.node_cfg.name}/{provider.source_type}"
242
+
243
+ def publish_hass_state(self, discovery: Discovery, in_progress: bool = False) -> None:
244
+ self.log.debug("HASS State update, in progress: %s, discovery: %s", in_progress, discovery)
245
+ self.publish(
246
+ self.state_topic(discovery),
247
+ hass_format_state(
248
+ discovery,
249
+ self.node_cfg.name,
250
+ discovery.session,
251
+ in_progress=in_progress,
252
+ ),
253
+ )
254
+
255
+ def publish_hass_config(self, discovery: Discovery) -> None:
256
+ object_id = f"{discovery.source_type}_{self.node_cfg.name}_{discovery.name}"
257
+ command_topic: str | None = self.command_topic(discovery.provider) if discovery.can_update else None
258
+ self.publish(
259
+ self.config_topic(discovery),
260
+ hass_format_config(
261
+ discovery,
262
+ object_id,
263
+ self.node_cfg.name,
264
+ self.state_topic(discovery),
265
+ command_topic,
266
+ discovery.session,
267
+ ),
268
+ )
269
+
270
+ def subscribe_hass_command(self, provider: ReleaseProvider): # noqa: ANN201
271
+ topic = self.command_topic(provider)
272
+ if topic in self.providers_by_topic or self.client is None:
273
+ self.log.debug("Skipping subscription", topic=topic)
274
+ else:
275
+ self.log.info("Handler subscribing", topic=topic)
276
+ self.providers_by_topic[topic] = provider
277
+ self.client.subscribe(topic)
278
+ return topic
279
+
280
+ def loop_once(self) -> None:
281
+ if self.client:
282
+ self.client.loop()
283
+
284
+ def publish(self, topic: str, payload: dict, qos: int = 0, retain: bool = True) -> None:
285
+ if self.client:
286
+ self.client.publish(topic, payload=json.dumps(payload), qos=qos, retain=retain)
updates2mqtt/py.typed ADDED
File without changes
@@ -0,0 +1,236 @@
1
+ Metadata-Version: 2.4
2
+ Name: updates2mqtt
3
+ Version: 1.3.4
4
+ Summary: System update and docker image notification and execution over MQTT
5
+ Project-URL: Repository, https://github.com/rhizomatics/updates2mqtt
6
+ Project-URL: Documentation, https://updates2mqtt.rhizomatics.org.uk
7
+ Project-URL: Issues, https://github.com/rhizomatics/updates2mqtt/issues
8
+ Author-email: jey burrows <jrb@rhizomatics.org.uk>
9
+ License-Expression: Apache-2.0
10
+ License-File: LICENSE
11
+ Keywords: automation,docker,home-assistant,homeassistant,mqtt,selfhosting,updates
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: System Administrators
15
+ Classifier: License :: OSI Approved :: Apache Software License
16
+ Classifier: License :: Other/Proprietary License
17
+ Classifier: Natural Language :: English
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Home Automation
22
+ Classifier: Topic :: System :: Monitoring
23
+ Classifier: Topic :: System :: Systems Administration
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.13
26
+ Requires-Dist: docker>=7.1.0
27
+ Requires-Dist: hishel[httpx]>=0.1.4
28
+ Requires-Dist: httpx>=0.28.1
29
+ Requires-Dist: omegaconf>=2.3.0
30
+ Requires-Dist: paho-mqtt>=2.1.0
31
+ Requires-Dist: rich>=14.0.0
32
+ Requires-Dist: structlog>=25.4.0
33
+ Requires-Dist: usingversion>=0.1.2
34
+ Description-Content-Type: text/markdown
35
+
36
+ [![Rhizomatics Open Source](https://avatars.githubusercontent.com/u/162821163?s=96&v=4)](https://github.com/rhizomatics)
37
+
38
+ # updates2mqtt
39
+
40
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/rhizomatics/supernotify)
41
+ [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/rhizomatics/updates2mqtt/main.svg)](https://results.pre-commit.ci/latest/github/rhizomatics/updates2mqtt/main)
42
+ [![Publish Python 🐍 distribution 📦 to PyPI and TestPyPI](https://github.com/rhizomatics/updates2mqtt/actions/workflows/pypi-publish.yml/badge.svg)](https://github.com/rhizomatics/updates2mqtt/actions/workflows/pypi-publish.yml)
43
+ [![Github Deploy](https://github.com/rhizomatics/updates2mqtt/actions/workflows/python-package.yml/badge.svg?branch=main)](https://github.com/rhizomatics/updates2mqtt/actions/workflows/python-package.yml)
44
+ [![CodeQL](https://github.com/rhizomatics/updates2mqtt/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/rhizomatics/updates2mqtt/actions/workflows/github-code-scanning/codeql)
45
+ [![Dependabot Updates](https://github.com/rhizomatics/updates2mqtt/actions/workflows/dependabot/dependabot-updates/badge.svg)](https://github.com/rhizomatics/updates2mqtt/actions/workflows/dependabot/dependabot-updates)
46
+
47
+ ## Summary
48
+
49
+ Use Home Assistant to notify you of updates to Docker images for your containers and optionally perform the *pull* (or optionally *build*) and *update*.
50
+
51
+ ![Example Home Assistant update dialog](images/ha_update_detail.png "Home Assistant Updates")
52
+
53
+ ## Description
54
+
55
+ updates2mqtt perioidically checks for new versions of components being available, and publishes new version info to MQTT.
56
+ HomeAssistant auto discovery is supported, so all updates can be seen in the same place as Home Assistant's
57
+ own components and add-ins.
58
+
59
+ Currently only Docker containers are supported, either via an image registry check, or a git repo for source. The design is modular, so other update sources can be added, at least for notification. The next anticipated is **apt** for Debian based systems.
60
+
61
+ Components can also be updated, either automatically or triggered via MQTT, for example by hitting the *Install*
62
+ button in the HomeAssistant update dialog. Icons and release notes can be specified for a better HA experience.
63
+
64
+ ## Install
65
+
66
+ updates2mqtt prefers to be run inside a Docker container.
67
+
68
+ ### Manual
69
+ ```
70
+ uv sync
71
+ uv run updates2mqtt
72
+ ```
73
+ ### Docker
74
+
75
+ See `examples` directory for a working `docker-compose.yaml`.
76
+
77
+ If you want to update and restart containers, then the file system paths to the location of the
78
+ directory where the docker compose file lives must be available in the updates2mqtt container.
79
+
80
+ The example `docker-compose.yaml` mounts `/home/containers` for this purpose, so if your containers are in
81
+ `/home/containers/app1`, `/home/containers/app2` etc, then updates2mqtt will be able to find them. Map as
82
+ many root paths as needed.
83
+
84
+ ## Configuration
85
+
86
+ Create file `config.yaml` in `conf` directory. If the file is not present, a default file will be generated.
87
+
88
+ ### Example configuration file
89
+
90
+ This is a maximal config file, the minimum is no config file at all, which will generate a default config file. The only mandatory values are the MQTT user name and password, everything else can be omitted.
91
+
92
+ ```yaml
93
+
94
+ node:
95
+ name: docker-host-1 # Unique name for this instance, used to name MQTT entities. Defaults to O/S hostname
96
+ git_repo_path: /usr/bin/git # Path to git inside container, needed only if non-default and using local docker builds
97
+ healthcheck:
98
+ enabled: true
99
+ interval: 300 # publish a heartbeat every 5 minutes
100
+ topic_template: healthcheck/{node_name}/updates2mqtt
101
+ mqtt:
102
+ host: ${oc.env:MQTT_HOST}
103
+ user: ${oc.env:MQTT_USER}
104
+ password: ${oc.env:MQTT_PASS}$ # Use an environment variable for secrets
105
+ port: ${oc.env:MQTT_PORT}
106
+ topic_root: updates2mqtt
107
+ homeassistant:
108
+ discovery:
109
+ prefix: homeassistant # Matches the default MQTT discovery prefix in Home Assistant
110
+ enabled: true
111
+ state_topic_suffix: state
112
+ docker:
113
+ enabled: true
114
+ allow_pull: true # if true, will do a `docker pull` if an update is available
115
+ allow_restart: true # if true, will do a `docker-compose up` if an update is installed
116
+ allow_build: true # if true, will do a `docker-compose build` if a git repo is configured
117
+ compose_version: v2 # Controls whether to use `docker-compose` (v1) or `docker compose` (v2) command
118
+ default_entity_picture_url: https://www.docker.com/wp-content/uploads/2022/03/Moby-logo.png # Picture for update dialog
119
+ device_icon: mdi:docker # Material Design Icon to use when browsing entities in Home Assistant
120
+ # device_icon: mdi:train-car-container # Alternative icon if you don't like Docker branding
121
+ discover_metadata:
122
+ linuxserver.io:
123
+ enabled: true
124
+ cache_ttl: 604800 # cache metadata for 1 week
125
+ scan_interval: 10800 # sleep interval between scan runs, in seconds
126
+ log:
127
+ level: INFO
128
+ ```
129
+
130
+ ### Moving Secrets Out of Config
131
+
132
+ Example use of environment variables, e.g. for secrets:
133
+
134
+ ```
135
+ mqtt:
136
+ password: ${oc.env:MQTT_PASS}
137
+ ```
138
+ ### Customizing images and release notes
139
+
140
+ Individual docker containers can have customized entity pictures or release notes, using env variables, for example in the `docker-compose.yaml` or in a separate `.env` file:
141
+
142
+ ```
143
+ environment:
144
+ - UPD2MQTT_PICTURE=https://frigate.video/images/logo.svg
145
+ - UPD2MQTT_RELNOTES=https://github.com/blakeblackshear/frigate/releases
146
+ ```
147
+
148
+ The images will show up in the *Update* section of *Settings* menu in HomeAssistant,
149
+ as will the release notes link. SVG icons should be used.
150
+
151
+ Some popular services have the icon and release note links pre-configured, in `common_packages.yaml`,
152
+ and packages from `linuxserver.io` can have metadata automatically discovered.
153
+
154
+ #### Icon Sources
155
+
156
+ - [Homarr Dashboard Icons](https://github.com/homarr-labs/dashboard-icons)
157
+ - [Self Hosted Icons](https://github.com/selfhst/icons)
158
+ - [Simple Icons](https://github.com/simple-icons/simple-icons)
159
+ - [Tabler Icons](https://tabler.io/icons)
160
+ - [Papirus Icons](https://github.com/PapirusDevelopmentTeam/papirus-icon-theme)
161
+ - [Homelab SVG Assets](https://github.com/loganmarchione/homelab-svg-assets)
162
+
163
+ ### Automated updates
164
+
165
+ If Docker containers should be immediately updated, without any confirmation
166
+ or trigger, *e.g.* from the HomeAssistant update dialog, then set an environment variable `UPD2MQTT_UPDATE`
167
+ in the target container to `Auto` ( it defaults to `Passive`)
168
+
169
+ ### Custom docker builds
170
+
171
+ If the image is locally built from a checked out git repo, package update can be driven
172
+ by the availability of git repo changes to pull rather than a new image on a Docker registry.
173
+
174
+ Declare the git path using the env var in ``UPD2MQTT_GIT_REPO_PATH`` in the docker container ( directly or via an ``.env`` file).
175
+ The git repo at this path will be used as the source of timestamps, and an update command will carry out a
176
+ ``git pull`` and ``docker-compose build`` rather than pulling an image.
177
+
178
+ Note that the updates2mqtt docker container needs access to this path declared in its volumes, and that has to
179
+ be read/write if automated install required.
180
+
181
+ ### Environment Variables
182
+
183
+ The following environment variables can be used to configure updates2mqtt:
184
+
185
+ | Env Var | Description | Default |
186
+ |---------| ------------|----------|
187
+ | `UPD2MQTT_UPDATE` | Update mode, either `Passive` or `Auto`. If `Auto`, updates will be installed automatically. | `Passive` |
188
+ | `UPD2MQTT_PICTURE` | URL to an icon to use in Home Assistant. | Docker logo URL |
189
+ | `UPD2MQTT_RELNOTES` | URL to release notes for the package. | |
190
+ | `UPD2MQTT_GIT_REPO_PATH` | Relative path to a local git repo if the image is built locally. | |
191
+ | `UPD2MQTT_IGNORE` | If set to `True`, the container will be ignored by updates2mqtt. | False |
192
+
193
+
194
+ ## Release Support
195
+
196
+ | Ecosystem | Support | Comments |
197
+ |-----------|-------------|----------------------------------------------------------------------------------------------------|
198
+ | Docker | Scan. Fetch | Fetch is ``docker pull`` only. Restart support only for ``docker-compose`` image based containers. |
199
+
200
+ ## Healthcheck
201
+
202
+ A heartbeat JSON payload is optionally published periodically to a configurable MQTT topic, defaulting to `healthcheck/{node_name}/updates2mqtt`. It contains the current version of updates2mqtt, the node name, a timestamp, and some basic stats.
203
+
204
+ A `healthcheck.sh` script is included in the Docker image, and can be used as a Docker healthcheck, if the container environment variables are set for `MQTT_HOST`, `MQTT_PORT`, `MQTT_USER` and `MQTT_PASS`.
205
+
206
+ TIP: Check healthcheck is working using `docker inspect --format "{{json .State.Health }}" updates2mqtt | jq`
207
+
208
+ ## HomeAssistant integration
209
+
210
+ Any updates that have support for automated install will automatically show in the
211
+ Home Assistant settings page if the [MQTT Integration](https://www.home-assistant.io/integrations/mqtt/) is installed and automatic discovery is not disabled.
212
+
213
+ ![Home Assistant MQTT Integraion configuration](images/ha_mqtt_discovery.png "Home Assistant MQTT Discovery")
214
+
215
+ The `homeassistant` default topic prefix matches the default updates2mqtt config, if its changed in HomeAssistant, then the updates2mqtt config must be changed to match.
216
+
217
+ ![Home Assistant updates in Settings](images/ha_update_page.png "Home Assistant Updates")
218
+
219
+ For Home Assistant integration, updates2mqtt represents each component being managed as a [MQTT Update](https://www.home-assistant.io/integrations/update.mqtt/) entity, and uses [MQTT discovery(https://www.home-assistant.io/integrations/mqtt/#mqtt-discovery)] so that HomeAssistant automatically picks up components discovered by updates2mqtt with zero configuration on HomeAssistant itself.
220
+
221
+ There are 3 separate types of MQTT topic used for HomeAssisstant integration:
222
+
223
+ - *Config* to support auto discovery. A topic is created per component, with a name like `homeassistant/update/dockernuc_docker_jellyfin/update/config`. This can be disabled in the config file, and the `homeassistant` topic prefix can also be configured.
224
+ - *State* to report the current version and the latest version available, again one topic per component, like `updates2mqtt/dockernuc/docker/jellyfin`.
225
+ - *Command* to support triggering an update. These will be created on the fly by HomeAssistant when an update is requested, and updates2mqtt subscribes to pick up the changes, so you won't typically see these if browsing MQTT topics. Only one is needed per updates2mqtt agent, with a name like `updates2mqtt/dockernuc/docker`
226
+
227
+ If the package supports automated update, then *Skip* and *Install* buttons will appear on the Home Assistant
228
+ interface, and the package can be remotely fetched and the component restarted.
229
+
230
+ ## Related Projects
231
+
232
+ - [psmqtt](https://github.com/eschava/psmqtt) - Report system health and metrics via MQTT
233
+ -
234
+ ## Development
235
+
236
+ Access to Docker APIs uses the Python [docker-py](https://docker-py.readthedocs.io/en/stable/) SDK for Python. [Eclipse Paho](https://eclipse.dev/paho/files/paho.mqtt.python/html/client.html) is used for MQTT access, and [OmegaConf](https://omegaconf.readthedocs.io) for configuration.
@@ -0,0 +1,16 @@
1
+ updates2mqtt/__init__.py,sha256=gnmHrLOSYc-N1-c5VG46OpNpoXEybKzYhEvFMm955P8,237
2
+ updates2mqtt/__main__.py,sha256=HBF00oH5fhS33sI_CdbxNlaUvbIzuuGxwnRYdhHqx0M,194
3
+ updates2mqtt/app.py,sha256=dfoqIIFq0kAm0KHjtEF_RrNC9-fJBQ7Y2_8suwWBOJU,7738
4
+ updates2mqtt/config.py,sha256=TIybHWpGAPLA1uWsZt0miwzboX1AlCNHoGO_JoGMlBo,4270
5
+ updates2mqtt/hass_formatter.py,sha256=Ulfj8F0e_1QMmRuJzHsNM2WxHbz9sIkWOWjRq3kQZzs,2772
6
+ updates2mqtt/model.py,sha256=5tWlj3appGGZjkuBeYR2lb-kXoy5mzCn4P_EJQjnwok,3676
7
+ updates2mqtt/mqtt.py,sha256=kDpbq9jk0av6FqyZrSPtg5oQrbklI0BMMH8wxnAp0OI,12230
8
+ updates2mqtt/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ updates2mqtt/integrations/__init__.py,sha256=KmNTUxvVWvqI7rl4I0xZg7XaCmcMS2O4OSv-ClsWM4Q,109
10
+ updates2mqtt/integrations/docker.py,sha256=OX_sXtWVgUJfRSFi_tTBib4gvP_foQI4MoeTkTeLbZc,18868
11
+ updates2mqtt/integrations/git_utils.py,sha256=SkAp6XcvCHwaiy17t6F97kcWTjBd7RyEmfhz6M_EhP0,2196
12
+ updates2mqtt-1.3.4.dist-info/METADATA,sha256=K96yWjB_b3rp5ErGl__gefFWvcvLSMGMMmDmNlTKQ8I,12781
13
+ updates2mqtt-1.3.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
14
+ updates2mqtt-1.3.4.dist-info/entry_points.txt,sha256=Hc6NZ2dBevYSUKTJU6NOs8Mw7Vt0S-9lq5FuKb76NCc,54
15
+ updates2mqtt-1.3.4.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
16
+ updates2mqtt-1.3.4.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ updates2mqtt = updates2mqtt.app:run