updates2mqtt 1.5.1__py3-none-any.whl → 1.7.0__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/app.py +23 -11
- updates2mqtt/config.py +30 -18
- updates2mqtt/hass_formatter.py +7 -17
- updates2mqtt/integrations/docker.py +303 -151
- updates2mqtt/integrations/docker_enrich.py +344 -0
- updates2mqtt/integrations/git_utils.py +71 -14
- updates2mqtt/model.py +150 -16
- updates2mqtt/mqtt.py +28 -7
- {updates2mqtt-1.5.1.dist-info → updates2mqtt-1.7.0.dist-info}/METADATA +68 -33
- updates2mqtt-1.7.0.dist-info/RECORD +16 -0
- {updates2mqtt-1.5.1.dist-info → updates2mqtt-1.7.0.dist-info}/WHEEL +1 -1
- {updates2mqtt-1.5.1.dist-info → updates2mqtt-1.7.0.dist-info}/entry_points.txt +1 -0
- updates2mqtt-1.5.1.dist-info/RECORD +0 -16
- updates2mqtt-1.5.1.dist-info/licenses/LICENSE +0 -201
updates2mqtt/model.py
CHANGED
|
@@ -1,9 +1,33 @@
|
|
|
1
|
+
import datetime as dt
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
import time
|
|
1
5
|
from abc import abstractmethod
|
|
2
6
|
from collections.abc import AsyncGenerator, Callable
|
|
7
|
+
from enum import StrEnum
|
|
3
8
|
from threading import Event
|
|
4
9
|
from typing import Any
|
|
5
10
|
|
|
6
11
|
import structlog
|
|
12
|
+
from tzlocal import get_localzone
|
|
13
|
+
|
|
14
|
+
from updates2mqtt.config import NO_KNOWN_IMAGE, NodeConfig, PublishPolicy, Selector, UpdatePolicy
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def timestamp(time_value: float | None) -> str | None:
|
|
18
|
+
if time_value is None:
|
|
19
|
+
return None
|
|
20
|
+
try:
|
|
21
|
+
return dt.datetime.fromtimestamp(time_value, tz=get_localzone()).isoformat()
|
|
22
|
+
except: # noqa: E722
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class VersionPolicy(StrEnum):
|
|
27
|
+
AUTO = "AUTO"
|
|
28
|
+
VERSION = "VERSION"
|
|
29
|
+
DIGEST = "DIGEST"
|
|
30
|
+
VERSION_DIGEST = "VERSION_DIGEST"
|
|
7
31
|
|
|
8
32
|
|
|
9
33
|
class Discovery:
|
|
@@ -22,15 +46,18 @@ class Discovery:
|
|
|
22
46
|
can_build: bool = False,
|
|
23
47
|
can_restart: bool = False,
|
|
24
48
|
status: str = "on",
|
|
49
|
+
publish_policy: PublishPolicy = PublishPolicy.HOMEASSISTANT,
|
|
25
50
|
update_type: str | None = "Update",
|
|
26
|
-
update_policy:
|
|
27
|
-
|
|
51
|
+
update_policy: UpdatePolicy = UpdatePolicy.PASSIVE,
|
|
52
|
+
version_policy: VersionPolicy = VersionPolicy.AUTO,
|
|
28
53
|
release_url: str | None = None,
|
|
29
54
|
release_summary: str | None = None,
|
|
30
55
|
title_template: str = "{discovery.update_type} for {discovery.name} on {discovery.node}",
|
|
31
56
|
device_icon: str | None = None,
|
|
32
57
|
custom: dict[str, Any] | None = None,
|
|
33
58
|
features: list[str] | None = None,
|
|
59
|
+
throttled: bool = False,
|
|
60
|
+
previous: "Discovery|None" = None,
|
|
34
61
|
) -> None:
|
|
35
62
|
self.provider: ReleaseProvider = provider
|
|
36
63
|
self.source_type: str = provider.source_type
|
|
@@ -49,36 +76,96 @@ class Discovery:
|
|
|
49
76
|
self.device_icon: str | None = device_icon
|
|
50
77
|
self.update_type: str | None = update_type
|
|
51
78
|
self.status: str = status
|
|
52
|
-
self.
|
|
53
|
-
self.
|
|
79
|
+
self.publish_policy: PublishPolicy = publish_policy
|
|
80
|
+
self.update_policy: UpdatePolicy = update_policy
|
|
81
|
+
self.version_policy: VersionPolicy = version_policy
|
|
82
|
+
self.update_last_attempt: float | None = None
|
|
54
83
|
self.custom: dict[str, Any] = custom or {}
|
|
55
84
|
self.features: list[str] = features or []
|
|
85
|
+
self.throttled: bool = throttled
|
|
86
|
+
self.scan_count: int
|
|
87
|
+
self.first_timestamp: float
|
|
88
|
+
self.last_timestamp: float = time.time()
|
|
89
|
+
|
|
90
|
+
if previous:
|
|
91
|
+
self.update_last_attempt = previous.update_last_attempt
|
|
92
|
+
self.first_timestamp = previous.first_timestamp
|
|
93
|
+
self.scan_count = previous.scan_count + 1
|
|
94
|
+
else:
|
|
95
|
+
self.first_timestamp = time.time()
|
|
96
|
+
self.scan_count = 1
|
|
56
97
|
|
|
57
98
|
def __repr__(self) -> str:
|
|
58
99
|
"""Build a custom string representation"""
|
|
59
100
|
return f"Discovery('{self.name}','{self.source_type}',current={self.current_version},latest={self.latest_version})"
|
|
60
101
|
|
|
102
|
+
def __str__(self) -> str:
|
|
103
|
+
"""Dump the attrs"""
|
|
104
|
+
|
|
105
|
+
def stringify(v: Any) -> str | int | float | bool:
|
|
106
|
+
return str(v) if not isinstance(v, (str, int, float, bool)) else v
|
|
107
|
+
|
|
108
|
+
dump = {k: stringify(v) for k, v in self.__dict__.items()}
|
|
109
|
+
return json.dumps(dump)
|
|
110
|
+
|
|
61
111
|
@property
|
|
62
112
|
def title(self) -> str:
|
|
63
113
|
if self.title_template:
|
|
64
114
|
return self.title_template.format(discovery=self)
|
|
65
115
|
return self.name
|
|
66
116
|
|
|
117
|
+
def as_dict(self) -> dict[str, str | list | dict | bool | int | None]:
|
|
118
|
+
return {
|
|
119
|
+
"name": self.name,
|
|
120
|
+
"node": self.node,
|
|
121
|
+
"provider": {"source_type": self.provider.source_type},
|
|
122
|
+
"first_scan": {"timestamp": timestamp(self.first_timestamp)},
|
|
123
|
+
"last_scan": {"timestamp": timestamp(self.last_timestamp), "session": self.session, "throttled": self.throttled},
|
|
124
|
+
"scan_count": self.scan_count,
|
|
125
|
+
"installed_version": self.current_version,
|
|
126
|
+
"latest_version": self.latest_version,
|
|
127
|
+
"title": self.title,
|
|
128
|
+
"release_summary": self.release_summary,
|
|
129
|
+
"release_url": self.release_url,
|
|
130
|
+
"entity_picture_url": self.entity_picture_url,
|
|
131
|
+
"can_update": self.can_update,
|
|
132
|
+
"can_build": self.can_build,
|
|
133
|
+
"can_restart": self.can_restart,
|
|
134
|
+
"device_icon": self.device_icon,
|
|
135
|
+
"update_type": self.update_type,
|
|
136
|
+
"status": self.status,
|
|
137
|
+
"features": self.features,
|
|
138
|
+
"update_policy": self.update_policy,
|
|
139
|
+
"publish_policy": self.publish_policy,
|
|
140
|
+
"version_policy": self.version_policy,
|
|
141
|
+
"update": {"last_attempt": timestamp(self.update_last_attempt), "in_progress": False},
|
|
142
|
+
self.source_type: self.custom,
|
|
143
|
+
}
|
|
144
|
+
|
|
67
145
|
|
|
68
146
|
class ReleaseProvider:
|
|
69
147
|
"""Abstract base class for release providers, such as container scanners or package managers API calls"""
|
|
70
148
|
|
|
71
|
-
def __init__(self, source_type: str = "base") -> None:
|
|
149
|
+
def __init__(self, node_cfg: NodeConfig, source_type: str = "base") -> None:
|
|
72
150
|
self.source_type: str = source_type
|
|
73
151
|
self.discoveries: dict[str, Discovery] = {}
|
|
152
|
+
self.node_cfg: NodeConfig = node_cfg
|
|
74
153
|
self.log: Any = structlog.get_logger().bind(integration=self.source_type)
|
|
75
154
|
self.stopped = Event()
|
|
76
155
|
|
|
156
|
+
def initialize(self) -> None:
|
|
157
|
+
"""Initialize any loops or background tasks, make any startup API calls"""
|
|
158
|
+
pass
|
|
159
|
+
|
|
77
160
|
def stop(self) -> None:
|
|
78
161
|
"""Stop any loops or background tasks"""
|
|
79
162
|
self.log.info("Asking release provider to stop", source_type=self.source_type)
|
|
80
163
|
self.stopped.set()
|
|
81
164
|
|
|
165
|
+
def __str__(self) -> str:
|
|
166
|
+
"""Stringify"""
|
|
167
|
+
return f"{self.source_type} Discovery"
|
|
168
|
+
|
|
82
169
|
@abstractmethod
|
|
83
170
|
def update(self, discovery: Discovery) -> bool:
|
|
84
171
|
"""Attempt to update the component version"""
|
|
@@ -90,18 +177,9 @@ class ReleaseProvider:
|
|
|
90
177
|
@abstractmethod
|
|
91
178
|
async def scan(self, session: str) -> AsyncGenerator[Discovery]:
|
|
92
179
|
"""Scan for components to monitor"""
|
|
93
|
-
raise NotImplementedError
|
|
94
180
|
# force recognition as an async generator
|
|
95
|
-
if False:
|
|
96
|
-
yield 0
|
|
97
|
-
|
|
98
|
-
def hass_config_format(self, discovery: Discovery) -> dict:
|
|
99
|
-
_ = discovery
|
|
100
|
-
return {}
|
|
101
|
-
|
|
102
|
-
def hass_state_format(self, discovery: Discovery) -> dict:
|
|
103
|
-
_ = discovery
|
|
104
|
-
return {}
|
|
181
|
+
if False:
|
|
182
|
+
yield 0 # type: ignore[unreachable]
|
|
105
183
|
|
|
106
184
|
@abstractmethod
|
|
107
185
|
def command(self, discovery_name: str, command: str, on_update_start: Callable, on_update_end: Callable) -> bool:
|
|
@@ -110,3 +188,59 @@ class ReleaseProvider:
|
|
|
110
188
|
@abstractmethod
|
|
111
189
|
def resolve(self, discovery_name: str) -> Discovery | None:
|
|
112
190
|
"""Resolve a discovered component by name"""
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class Selection:
|
|
194
|
+
def __init__(self, selector: Selector, value: str | None) -> None:
|
|
195
|
+
self.result: bool = True
|
|
196
|
+
self.matched: str | None = None
|
|
197
|
+
if value is None:
|
|
198
|
+
self.result = selector.include is None
|
|
199
|
+
return
|
|
200
|
+
if selector.exclude is not None:
|
|
201
|
+
self.result = True
|
|
202
|
+
if any(re.search(pat, value) for pat in selector.exclude):
|
|
203
|
+
self.matched = value
|
|
204
|
+
self.result = False
|
|
205
|
+
if selector.include is not None:
|
|
206
|
+
self.result = False
|
|
207
|
+
if any(re.search(pat, value) for pat in selector.include):
|
|
208
|
+
self.matched = value
|
|
209
|
+
self.result = True
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
VERSION_RE = r"[vV]?[0-9]+(\.[0-9]+)*"
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def select_version(
|
|
216
|
+
version_policy: VersionPolicy,
|
|
217
|
+
version: str | None,
|
|
218
|
+
digest: str | None,
|
|
219
|
+
other_version: str | None = None,
|
|
220
|
+
other_digest: str | None = None,
|
|
221
|
+
) -> str:
|
|
222
|
+
if version_policy == VersionPolicy.VERSION and version:
|
|
223
|
+
return version
|
|
224
|
+
if version_policy == VersionPolicy.DIGEST and digest and digest != NO_KNOWN_IMAGE:
|
|
225
|
+
return digest
|
|
226
|
+
if version_policy == VersionPolicy.VERSION_DIGEST and version and digest and digest != NO_KNOWN_IMAGE:
|
|
227
|
+
return f"{version} ({digest})"
|
|
228
|
+
# AUTO or fallback
|
|
229
|
+
if version_policy == VersionPolicy.AUTO and version and re.match(VERSION_RE, version or ""):
|
|
230
|
+
# Smells like semver
|
|
231
|
+
if other_version is None and other_digest is None:
|
|
232
|
+
return version
|
|
233
|
+
if re.match(VERSION_RE, other_version or "") and (
|
|
234
|
+
(version == other_version and digest == other_digest) or (version != other_version and digest != other_digest)
|
|
235
|
+
):
|
|
236
|
+
# Only semver if versions and digest consistently same or different
|
|
237
|
+
return version
|
|
238
|
+
|
|
239
|
+
if version and digest and digest != NO_KNOWN_IMAGE:
|
|
240
|
+
return f"{version}:{digest}"
|
|
241
|
+
if version:
|
|
242
|
+
return version
|
|
243
|
+
if digest and digest != NO_KNOWN_IMAGE:
|
|
244
|
+
return digest
|
|
245
|
+
|
|
246
|
+
return other_version or other_version or NO_KNOWN_IMAGE
|
updates2mqtt/mqtt.py
CHANGED
|
@@ -16,7 +16,7 @@ from paho.mqtt.reasoncodes import ReasonCode
|
|
|
16
16
|
|
|
17
17
|
from updates2mqtt.model import Discovery, ReleaseProvider
|
|
18
18
|
|
|
19
|
-
from .config import HomeAssistantConfig, MqttConfig, NodeConfig
|
|
19
|
+
from .config import HomeAssistantConfig, MqttConfig, NodeConfig, PublishPolicy
|
|
20
20
|
from .hass_formatter import hass_format_config, hass_format_state
|
|
21
21
|
|
|
22
22
|
log = structlog.get_logger()
|
|
@@ -193,11 +193,11 @@ class MqttPublisher:
|
|
|
193
193
|
try:
|
|
194
194
|
return json.loads(jsonish)
|
|
195
195
|
except Exception:
|
|
196
|
-
log.exception("JSON decode fail (%s)
|
|
196
|
+
log.exception("JSON decode fail (%s)", jsonish)
|
|
197
197
|
try:
|
|
198
198
|
return json.loads(jsonish[1:-1])
|
|
199
199
|
except Exception:
|
|
200
|
-
log.exception("JSON decode fail (%s)
|
|
200
|
+
log.exception("JSON decode fail (%s)", jsonish[1:-1])
|
|
201
201
|
return {}
|
|
202
202
|
|
|
203
203
|
async def execute_command(
|
|
@@ -235,7 +235,10 @@ class MqttPublisher:
|
|
|
235
235
|
updated = provider.command(comp_name, command, on_update_start, on_update_end)
|
|
236
236
|
discovery = provider.resolve(comp_name)
|
|
237
237
|
if updated and discovery:
|
|
238
|
-
|
|
238
|
+
if discovery.publish_policy in (PublishPolicy.HOMEASSISTANT, PublishPolicy.MQTT):
|
|
239
|
+
self.publish_discovery(discovery)
|
|
240
|
+
if discovery.publish_policy == PublishPolicy.HOMEASSISTANT:
|
|
241
|
+
self.publish_hass_state(discovery)
|
|
239
242
|
else:
|
|
240
243
|
logger.debug("No change to republish after execution")
|
|
241
244
|
logger.info("Execution ended")
|
|
@@ -283,10 +286,12 @@ class MqttPublisher:
|
|
|
283
286
|
|
|
284
287
|
def handle_message(self, msg: mqtt.MQTTMessage | LocalMessage) -> None:
|
|
285
288
|
def update_start(discovery: Discovery) -> None:
|
|
286
|
-
|
|
289
|
+
if discovery.publish_policy in (PublishPolicy.HOMEASSISTANT, PublishPolicy.MQTT):
|
|
290
|
+
self.publish_hass_state(discovery, in_progress=True)
|
|
287
291
|
|
|
288
292
|
def update_end(discovery: Discovery) -> None:
|
|
289
|
-
|
|
293
|
+
if discovery.publish_policy in (PublishPolicy.HOMEASSISTANT, PublishPolicy.MQTT):
|
|
294
|
+
self.publish_hass_state(discovery, in_progress=False)
|
|
290
295
|
|
|
291
296
|
if self.event_loop is not None:
|
|
292
297
|
asyncio.run_coroutine_threadsafe(self.execute_command(msg, update_start, update_end), self.event_loop)
|
|
@@ -298,12 +303,26 @@ class MqttPublisher:
|
|
|
298
303
|
return f"{prefix}/update/{self.node_cfg.name}_{discovery.source_type}_{discovery.name}/update/config"
|
|
299
304
|
|
|
300
305
|
def state_topic(self, discovery: Discovery) -> str:
|
|
306
|
+
return f"{self.cfg.topic_root}/{self.node_cfg.name}/{discovery.source_type}/{discovery.name}/state"
|
|
307
|
+
|
|
308
|
+
def general_topic(self, discovery: Discovery) -> str:
|
|
301
309
|
return f"{self.cfg.topic_root}/{self.node_cfg.name}/{discovery.source_type}/{discovery.name}"
|
|
302
310
|
|
|
303
311
|
def command_topic(self, provider: ReleaseProvider) -> str:
|
|
304
312
|
return f"{self.cfg.topic_root}/{self.node_cfg.name}/{provider.source_type}"
|
|
305
313
|
|
|
314
|
+
def publish_discovery(self, discovery: Discovery, in_progress: bool = False) -> None:
|
|
315
|
+
"""Comprehensive, non Home Assistant specific, base publication"""
|
|
316
|
+
if discovery.publish_policy not in (PublishPolicy.HOMEASSISTANT, PublishPolicy.MQTT):
|
|
317
|
+
return
|
|
318
|
+
self.log.debug("Discovery publish: %s", discovery)
|
|
319
|
+
payload: dict[str, Any] = discovery.as_dict()
|
|
320
|
+
payload["update"]["in_progress"] = in_progress # ty:ignore[invalid-assignment]
|
|
321
|
+
self.publish(self.general_topic(discovery), payload)
|
|
322
|
+
|
|
306
323
|
def publish_hass_state(self, discovery: Discovery, in_progress: bool = False) -> None:
|
|
324
|
+
if discovery.publish_policy != PublishPolicy.HOMEASSISTANT:
|
|
325
|
+
return
|
|
307
326
|
self.log.debug("HASS State update, in progress: %s, discovery: %s", in_progress, discovery)
|
|
308
327
|
self.publish(
|
|
309
328
|
self.state_topic(discovery),
|
|
@@ -315,6 +334,8 @@ class MqttPublisher:
|
|
|
315
334
|
)
|
|
316
335
|
|
|
317
336
|
def publish_hass_config(self, discovery: Discovery) -> None:
|
|
337
|
+
if discovery.publish_policy != PublishPolicy.HOMEASSISTANT:
|
|
338
|
+
return
|
|
318
339
|
object_id = f"{discovery.source_type}_{self.node_cfg.name}_{discovery.name}"
|
|
319
340
|
self.publish(
|
|
320
341
|
self.config_topic(discovery),
|
|
@@ -323,10 +344,10 @@ class MqttPublisher:
|
|
|
323
344
|
object_id=object_id,
|
|
324
345
|
area=self.hass_cfg.area,
|
|
325
346
|
state_topic=self.state_topic(discovery),
|
|
347
|
+
attrs_topic=self.general_topic(discovery) if self.hass_cfg.extra_attributes else None,
|
|
326
348
|
command_topic=self.command_topic(discovery.provider),
|
|
327
349
|
force_command_topic=self.hass_cfg.force_command_topic,
|
|
328
350
|
device_creation=self.hass_cfg.device_creation,
|
|
329
|
-
session=discovery.session,
|
|
330
351
|
),
|
|
331
352
|
)
|
|
332
353
|
|
|
@@ -1,38 +1,39 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: updates2mqtt
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.7.0
|
|
4
4
|
Summary: System update and docker image notification and execution over MQTT
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Project-URL: Documentation, https://updates2mqtt.rhizomatics.org.uk
|
|
8
|
-
Project-URL: Issues, https://github.com/rhizomatics/updates2mqtt/issues
|
|
9
|
-
Project-URL: Changelog, https://github.com/rhizomatics/updates2mqtt/blob/main/CHANGELOG.md
|
|
5
|
+
Keywords: mqtt,docker,oci,container,updates,automation,home-assistant,homeassistant,selfhosting
|
|
6
|
+
Author: jey burrows
|
|
10
7
|
Author-email: jey burrows <jrb@rhizomatics.org.uk>
|
|
11
8
|
License-Expression: Apache-2.0
|
|
12
|
-
License-File: LICENSE
|
|
13
|
-
Keywords: automation,docker,home-assistant,homeassistant,mqtt,selfhosting,updates
|
|
14
9
|
Classifier: Development Status :: 5 - Production/Stable
|
|
15
|
-
Classifier: Environment :: Console
|
|
16
|
-
Classifier: Intended Audience :: System Administrators
|
|
17
|
-
Classifier: License :: OSI Approved :: Apache Software License
|
|
18
10
|
Classifier: License :: Other/Proprietary License
|
|
19
11
|
Classifier: Natural Language :: English
|
|
20
12
|
Classifier: Operating System :: OS Independent
|
|
21
|
-
Classifier:
|
|
22
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Environment :: Console
|
|
23
14
|
Classifier: Topic :: Home Automation
|
|
24
|
-
Classifier: Topic :: System :: Monitoring
|
|
25
15
|
Classifier: Topic :: System :: Systems Administration
|
|
16
|
+
Classifier: Topic :: System :: Monitoring
|
|
17
|
+
Classifier: Intended Audience :: System Administrators
|
|
18
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
26
19
|
Classifier: Typing :: Typed
|
|
27
|
-
|
|
20
|
+
Classifier: Programming Language :: Python
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
28
22
|
Requires-Dist: docker>=7.1.0
|
|
29
|
-
Requires-Dist: hishel[httpx]>=0.1.4
|
|
30
|
-
Requires-Dist: httpx>=0.28.1
|
|
31
|
-
Requires-Dist: omegaconf>=2.3.0
|
|
32
23
|
Requires-Dist: paho-mqtt>=2.1.0
|
|
33
|
-
Requires-Dist:
|
|
24
|
+
Requires-Dist: omegaconf>=2.3.0
|
|
34
25
|
Requires-Dist: structlog>=25.4.0
|
|
26
|
+
Requires-Dist: rich>=14.0.0
|
|
27
|
+
Requires-Dist: httpx>=0.28.1
|
|
28
|
+
Requires-Dist: hishel[httpx]>=0.1.4
|
|
35
29
|
Requires-Dist: usingversion>=0.1.2
|
|
30
|
+
Requires-Dist: tzlocal>=5.3.1
|
|
31
|
+
Requires-Python: >=3.13
|
|
32
|
+
Project-URL: Homepage, https://updates2mqtt.rhizomatics.org.uk
|
|
33
|
+
Project-URL: Repository, https://github.com/rhizomatics/updates2mqtt
|
|
34
|
+
Project-URL: Documentation, https://updates2mqtt.rhizomatics.org.uk
|
|
35
|
+
Project-URL: Issues, https://github.com/rhizomatics/updates2mqtt/issues
|
|
36
|
+
Project-URL: Changelog, https://github.com/rhizomatics/updates2mqtt/blob/main/CHANGELOG.md
|
|
36
37
|
Description-Content-Type: text/markdown
|
|
37
38
|
|
|
38
39
|
{ align=left }
|
|
@@ -42,7 +43,7 @@ Description-Content-Type: text/markdown
|
|
|
42
43
|
[](https://github.com/rhizomatics)
|
|
43
44
|
|
|
44
45
|
[](https://pypi.org/project/updates2mqtt/)
|
|
45
|
-
[](https://github.com/rhizomatics/
|
|
46
|
+
[](https://github.com/rhizomatics/updates2mqtt)
|
|
46
47
|
[](https://updates2mqtt.rhizomatics.org.uk/developer/coverage/)
|
|
47
48
|

|
|
48
49
|
[](https://results.pre-commit.ci/latest/github/rhizomatics/updates2mqtt/main)
|
|
@@ -69,7 +70,7 @@ Read the release notes, and optionally click *Update* to trigger a Docker *pull*
|
|
|
69
70
|
|
|
70
71
|
## Description
|
|
71
72
|
|
|
72
|
-
|
|
73
|
+
Updates2MQTT perioidically checks for new versions of components being available, and publishes new version info to MQTT. HomeAssistant auto discovery is supported, so all updates can be seen in the same place as Home Assistant's own components and add-ins.
|
|
73
74
|
|
|
74
75
|
Currently only Docker containers are supported, either via an image registry check, or a git repo for source (see [Local Builds](local_builds.md)). The design is modular, so other update sources can be added, at least for notification. The next anticipated is **apt** for Debian based systems.
|
|
75
76
|
|
|
@@ -79,8 +80,14 @@ To get started, read the [Installation](installation.md) and [Configuration](con
|
|
|
79
80
|
|
|
80
81
|
For a quick spin, try this:
|
|
81
82
|
|
|
82
|
-
```
|
|
83
|
-
docker run -e MQTT_USER=user1 -e MQTT_PASS=
|
|
83
|
+
```bash
|
|
84
|
+
docker run -v /var/run/docker.sock:/var/run/docker.sock -e MQTT_USER=user1 -e MQTT_PASS=user1 -e MQTT_HOST=192.168.1.5 ghcr.io/rhizomatics/updates2mqtt:release
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
or without Docker, using [uv](https://docs.astral.sh/uv/)
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
export MQTT_HOST=192.168.1.1;export MQTT_USER=user1;export MQTT_PASS=user1;uv run --with updates2mqtt python -m updates2mqtt
|
|
84
91
|
```
|
|
85
92
|
|
|
86
93
|
## Release Support
|
|
@@ -93,7 +100,7 @@ Presently only Docker containers are supported, although others are planned, pro
|
|
|
93
100
|
|
|
94
101
|
## Heartbeat
|
|
95
102
|
|
|
96
|
-
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
|
|
103
|
+
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.
|
|
97
104
|
|
|
98
105
|
## Healthcheck
|
|
99
106
|
|
|
@@ -126,7 +133,8 @@ file, or as `environment` options inside `docker-compose.yaml`.
|
|
|
126
133
|
### Automated updates
|
|
127
134
|
|
|
128
135
|
If Docker containers should be immediately updated, without any confirmation
|
|
129
|
-
or trigger, *e.g.* from the HomeAssistant update dialog, then set an environment variable `UPD2MQTT_UPDATE` in the target container to `Auto` ( it defaults to `Passive`)
|
|
136
|
+
or trigger, *e.g.* from the HomeAssistant update dialog, then set an environment variable `UPD2MQTT_UPDATE` in the target container to `Auto` ( it defaults to `Passive`). If you want it to update without publishing to MQTT and being
|
|
137
|
+
visible to Home Assistant, then use `Silent`.
|
|
130
138
|
|
|
131
139
|
```yaml title="Example Compose Snippet"
|
|
132
140
|
restarter:
|
|
@@ -136,17 +144,42 @@ restarter:
|
|
|
136
144
|
- UPD2MQTT_UPDATE=AUTO
|
|
137
145
|
```
|
|
138
146
|
|
|
147
|
+
Automated updates can also apply to local builds, where a `git_repo_path` has been defined - if there are remote
|
|
148
|
+
commits available to pull, then a `git pull`, `docker compose build` and `docker compose up` will be executed.
|
|
149
|
+
|
|
139
150
|
### Environment Variables
|
|
140
151
|
|
|
141
152
|
The following environment variables can be used to configure containers for `updates2mqtt`:
|
|
142
153
|
|
|
143
|
-
| Env Var
|
|
144
|
-
|
|
145
|
-
| `UPD2MQTT_UPDATE`
|
|
146
|
-
| `UPD2MQTT_PICTURE`
|
|
147
|
-
| `UPD2MQTT_RELNOTES`
|
|
148
|
-
| `UPD2MQTT_GIT_REPO_PATH`
|
|
149
|
-
| `UPD2MQTT_IGNORE`
|
|
154
|
+
| Env Var | Description | Default |
|
|
155
|
+
|----------------------------|----------------------------------------------------------------------------------------------|-----------------|
|
|
156
|
+
| `UPD2MQTT_UPDATE` | Update mode, either `Passive` or `Auto`. If `Auto`, updates will be installed automatically. | `Passive` |
|
|
157
|
+
| `UPD2MQTT_PICTURE` | URL to an icon to use in Home Assistant. | Docker logo URL |
|
|
158
|
+
| `UPD2MQTT_RELNOTES` | URL to release notes for the package. | |
|
|
159
|
+
| `UPD2MQTT_GIT_REPO_PATH` | Relative path to a local git repo if the image is built locally. | |
|
|
160
|
+
| `UPD2MQTT_IGNORE` | If set to `True`, the container will be ignored by Updates2MQTT. | False |
|
|
161
|
+
| |
|
|
162
|
+
|
|
163
|
+
### Docker Labels
|
|
164
|
+
|
|
165
|
+
Alternatively, use Docker labels
|
|
166
|
+
|
|
167
|
+
| Label | Env Var |
|
|
168
|
+
|--------------------------------|----------------------------|
|
|
169
|
+
| `updates2mqtt.update` | `UPD2MQTT_UPDATE` |
|
|
170
|
+
| `updates2mqtt.picture` | `UPD2MQTT_PCITURE` |
|
|
171
|
+
| `updates2mqtt.relnotes` | `UPD2MQTT_RELNOTES` |
|
|
172
|
+
| `updates2mqtt.git_repo_path` | `UPD2MQTT_GIT_REPO_PATH` |
|
|
173
|
+
| `updates2mqtt.ignore` | `UPD2MQTT_IGNORE` |
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
```yaml title="Example Compose Snippet"
|
|
177
|
+
restarter:
|
|
178
|
+
image: docker:cli
|
|
179
|
+
command: ["/bin/sh", "-c", "while true; do sleep 86400; docker restart mailserver; done"]
|
|
180
|
+
labels:
|
|
181
|
+
updates2mqtt.relnotes: https://component.my.com/release_notes
|
|
182
|
+
```
|
|
150
183
|
|
|
151
184
|
|
|
152
185
|
## Related Projects
|
|
@@ -157,6 +190,8 @@ Other apps useful for self-hosting with the help of MQTT:
|
|
|
157
190
|
|
|
158
191
|
Find more at [awesome-mqtt](https://github.com/rhizomatics/awesome-mqtt)
|
|
159
192
|
|
|
193
|
+
For a more powerful Docker update manager, try [What's Up Docker](https://getwud.github.io/wud/)
|
|
194
|
+
|
|
160
195
|
## Development
|
|
161
196
|
|
|
162
197
|
This component relies on several open source packages:
|
|
@@ -169,4 +204,4 @@ This component relies on several open source packages:
|
|
|
169
204
|
- [httpx](https://www.python-httpx.org) for retrieving metadata
|
|
170
205
|
- The Astral [uv](https://docs.astral.sh/uv/) and [ruff](https://docs.astral.sh/ruff/) tools for development and build
|
|
171
206
|
- [pytest](https://docs.pytest.org/en/stable/) and supporting add-ins for automated testing
|
|
172
|
-
- [usingversion](https://pypi.org/project/usingversion/) to log current version info
|
|
207
|
+
- [usingversion](https://pypi.org/project/usingversion/) to log current version info
|
|
@@ -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=4OOzVTuOw5Zxrm6zppRG6kq7x6bOY6S0h44yRnoYoVk,9651
|
|
4
|
+
updates2mqtt/config.py,sha256=Hulk-zynpWq6JUv_ftEY5tmY-Vr8tQU9vV8nuhKXmJ4,5936
|
|
5
|
+
updates2mqtt/hass_formatter.py,sha256=xRm_iJiHU02v4KxNwps3V3g4dl_A7wNvQpLHCODzZlM,2690
|
|
6
|
+
updates2mqtt/integrations/__init__.py,sha256=KmNTUxvVWvqI7rl4I0xZg7XaCmcMS2O4OSv-ClsWM4Q,109
|
|
7
|
+
updates2mqtt/integrations/docker.py,sha256=GBYmzfzXuoI6j9GGIiDdq4KnR4677RCgzfxF6r4vTRM,28099
|
|
8
|
+
updates2mqtt/integrations/docker_enrich.py,sha256=wdJOOvZw7gG5ncHy6BqeIc9vPhR94YJYa9dumLmh8SY,15279
|
|
9
|
+
updates2mqtt/integrations/git_utils.py,sha256=ODCKecWnom1NEKsmDZ2vFmYfnVGtWUgq7svVGcOtSaU,4369
|
|
10
|
+
updates2mqtt/model.py,sha256=MmWzHq8ibXYRVMLmATrmhMp05q3neAIkP8Q1OkEepBs,9264
|
|
11
|
+
updates2mqtt/mqtt.py,sha256=KC8VYQ_OYiCoFehLT90dS5n8ZOZfffThapmQ29kyifE,16384
|
|
12
|
+
updates2mqtt/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
updates2mqtt-1.7.0.dist-info/WHEEL,sha256=XV0cjMrO7zXhVAIyyc8aFf1VjZ33Fen4IiJk5zFlC3g,80
|
|
14
|
+
updates2mqtt-1.7.0.dist-info/entry_points.txt,sha256=Nt1kQQfJ1M2RvcRUnVxe3KCMiX8puHPqz-D7BwqV1L8,55
|
|
15
|
+
updates2mqtt-1.7.0.dist-info/METADATA,sha256=POaF66x0AbR8qLJFyR3tDv27xRIbAu0Msyt2_o26TGU,11363
|
|
16
|
+
updates2mqtt-1.7.0.dist-info/RECORD,,
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
updates2mqtt/__init__.py,sha256=gnmHrLOSYc-N1-c5VG46OpNpoXEybKzYhEvFMm955P8,237
|
|
2
|
-
updates2mqtt/__main__.py,sha256=HBF00oH5fhS33sI_CdbxNlaUvbIzuuGxwnRYdhHqx0M,194
|
|
3
|
-
updates2mqtt/app.py,sha256=pYcNprv7_htqD6wiXX5j24FhPglEeFNdqMfPclq5SfU,8887
|
|
4
|
-
updates2mqtt/config.py,sha256=nlAszykZa7t9nu99RB8g_tIzQxmdfWyilTZJY3Fl4e4,6099
|
|
5
|
-
updates2mqtt/hass_formatter.py,sha256=bj6qpElMqt1DqKlxp4ZjwaCJAD-ed3xsq5ZOg4FbeC8,3216
|
|
6
|
-
updates2mqtt/model.py,sha256=KNsLflgWaRvGrNdq1Vy2QnDLfVSLPWLSt4gBXj9COa4,4170
|
|
7
|
-
updates2mqtt/mqtt.py,sha256=j7nlSka-LqRethZk_rc4Z8iOV7ey28TQFjSfrPeTjv4,14995
|
|
8
|
-
updates2mqtt/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
-
updates2mqtt/integrations/__init__.py,sha256=KmNTUxvVWvqI7rl4I0xZg7XaCmcMS2O4OSv-ClsWM4Q,109
|
|
10
|
-
updates2mqtt/integrations/docker.py,sha256=Mi3tFjVDbCIsHBgH6UQ3yC5Y7HQk9JgEyH6PQuNJtFU,21263
|
|
11
|
-
updates2mqtt/integrations/git_utils.py,sha256=bPCmQiZpKpMcrGI7xAVmePXHFn8WwjcPNkf7xqDsGQA,2319
|
|
12
|
-
updates2mqtt-1.5.1.dist-info/METADATA,sha256=iPMKAgEFTFZ98pouv3SnTTStP74SEPBuV8jN3QbBpX8,9392
|
|
13
|
-
updates2mqtt-1.5.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
14
|
-
updates2mqtt-1.5.1.dist-info/entry_points.txt,sha256=Hc6NZ2dBevYSUKTJU6NOs8Mw7Vt0S-9lq5FuKb76NCc,54
|
|
15
|
-
updates2mqtt-1.5.1.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
16
|
-
updates2mqtt-1.5.1.dist-info/RECORD,,
|