updates2mqtt 1.6.0__py3-none-any.whl → 1.7.2__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 +10 -7
- updates2mqtt/cli.py +150 -0
- updates2mqtt/config.py +61 -20
- updates2mqtt/hass_formatter.py +12 -21
- updates2mqtt/helpers.py +226 -0
- updates2mqtt/integrations/docker.py +356 -301
- updates2mqtt/integrations/docker_enrich.py +876 -0
- updates2mqtt/integrations/git_utils.py +5 -5
- updates2mqtt/model.py +147 -24
- updates2mqtt/mqtt.py +31 -5
- {updates2mqtt-1.6.0.dist-info → updates2mqtt-1.7.2.dist-info}/METADATA +21 -19
- updates2mqtt-1.7.2.dist-info/RECORD +18 -0
- {updates2mqtt-1.6.0.dist-info → updates2mqtt-1.7.2.dist-info}/WHEEL +2 -2
- {updates2mqtt-1.6.0.dist-info → updates2mqtt-1.7.2.dist-info}/entry_points.txt +1 -0
- updates2mqtt-1.6.0.dist-info/RECORD +0 -15
|
@@ -44,7 +44,7 @@ def git_iso_timestamp(repo_path: Path, git_path: Path) -> str | None:
|
|
|
44
44
|
return None
|
|
45
45
|
|
|
46
46
|
|
|
47
|
-
def
|
|
47
|
+
def git_local_digest(repo_path: Path, git_path: Path) -> str | None:
|
|
48
48
|
result = None
|
|
49
49
|
try:
|
|
50
50
|
result = subprocess.run(
|
|
@@ -56,17 +56,17 @@ def git_local_version(repo_path: Path, git_path: Path) -> str | None:
|
|
|
56
56
|
check=True,
|
|
57
57
|
)
|
|
58
58
|
if result.returncode == 0:
|
|
59
|
-
log.debug("Local git rev-parse", action="
|
|
60
|
-
return
|
|
59
|
+
log.debug("Local git rev-parse", action="git_local_digest", path=repo_path, version=result.stdout.strip())
|
|
60
|
+
return result.stdout.strip()[:15]
|
|
61
61
|
except subprocess.CalledProcessError as cpe:
|
|
62
|
-
log.warn("GIT No result from git rev-parse at %s: %s", repo_path, cpe, action="
|
|
62
|
+
log.warn("GIT No result from git rev-parse at %s: %s", repo_path, cpe, action="git_local_digest")
|
|
63
63
|
except Exception as e:
|
|
64
64
|
log.error(
|
|
65
65
|
"GIT Unable to retrieve version at %s - %s: %s",
|
|
66
66
|
repo_path,
|
|
67
67
|
result.stdout if result else "<NO RESULT>",
|
|
68
68
|
e,
|
|
69
|
-
action="
|
|
69
|
+
action="git_local_digest",
|
|
70
70
|
)
|
|
71
71
|
return None
|
|
72
72
|
|
updates2mqtt/model.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import time
|
|
2
3
|
from abc import abstractmethod
|
|
3
4
|
from collections.abc import AsyncGenerator, Callable
|
|
4
5
|
from threading import Event
|
|
@@ -6,6 +7,58 @@ from typing import Any
|
|
|
6
7
|
|
|
7
8
|
import structlog
|
|
8
9
|
|
|
10
|
+
from updates2mqtt.config import NodeConfig, PublishPolicy, UpdatePolicy, VersionPolicy
|
|
11
|
+
from updates2mqtt.helpers import timestamp
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DiscoveryArtefactDetail:
|
|
15
|
+
"""Provider specific detail"""
|
|
16
|
+
|
|
17
|
+
def as_dict(self) -> dict[str, str | list | dict | bool | int | None]:
|
|
18
|
+
return {}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DiscoveryInstallationDetail:
|
|
22
|
+
"""Provider specific detail"""
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def as_dict(self) -> dict[str, str | list | dict | bool | int | None]:
|
|
26
|
+
return {}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ReleaseDetail:
|
|
30
|
+
"""The artefact source details
|
|
31
|
+
|
|
32
|
+
Note this may be an actual software package, or the source details of the wrapping of it
|
|
33
|
+
For example, some Docker images report the main source repo, and others where the Dockerfile deploy project lives
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, notes_url: str | None = None, summary: str | None = None) -> None:
|
|
37
|
+
self.source_platform: str | None = None
|
|
38
|
+
self.source_repo_url: str | None = None
|
|
39
|
+
self.source_url: str | None = None
|
|
40
|
+
self.version: str | None = None
|
|
41
|
+
self.revision: str | None = None
|
|
42
|
+
self.diff_url: str | None = None
|
|
43
|
+
self.notes_url: str | None = notes_url
|
|
44
|
+
self.title: str | None = None
|
|
45
|
+
self.summary: str | None = summary
|
|
46
|
+
self.net_score: int | None = None
|
|
47
|
+
|
|
48
|
+
def as_dict(self) -> dict[str, str | None]:
|
|
49
|
+
return {
|
|
50
|
+
"title": self.title,
|
|
51
|
+
"version": self.version,
|
|
52
|
+
"source_platform": self.source_platform,
|
|
53
|
+
"source_repo": self.source_repo_url,
|
|
54
|
+
"source": self.source_url,
|
|
55
|
+
"revision": self.revision,
|
|
56
|
+
"diff_url": self.diff_url,
|
|
57
|
+
"notes_url": self.notes_url,
|
|
58
|
+
"summary": self.summary,
|
|
59
|
+
"net_score": str(self.net_score) if self.net_score is not None else None,
|
|
60
|
+
}
|
|
61
|
+
|
|
9
62
|
|
|
10
63
|
class Discovery:
|
|
11
64
|
"""Discovered component from a scan"""
|
|
@@ -19,20 +72,24 @@ class Discovery:
|
|
|
19
72
|
entity_picture_url: str | None = None,
|
|
20
73
|
current_version: str | None = None,
|
|
21
74
|
latest_version: str | None = None,
|
|
22
|
-
can_update: bool = False,
|
|
23
75
|
can_build: bool = False,
|
|
24
76
|
can_restart: bool = False,
|
|
77
|
+
can_pull: bool = False,
|
|
25
78
|
status: str = "on",
|
|
79
|
+
publish_policy: PublishPolicy = PublishPolicy.HOMEASSISTANT,
|
|
26
80
|
update_type: str | None = "Update",
|
|
27
|
-
update_policy:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
release_summary: str | None = None,
|
|
81
|
+
update_policy: UpdatePolicy = UpdatePolicy.PASSIVE,
|
|
82
|
+
version_policy: VersionPolicy = VersionPolicy.AUTO,
|
|
83
|
+
version_basis: str | None = None,
|
|
31
84
|
title_template: str = "{discovery.update_type} for {discovery.name} on {discovery.node}",
|
|
32
85
|
device_icon: str | None = None,
|
|
33
86
|
custom: dict[str, Any] | None = None,
|
|
34
|
-
features: list[str] | None = None,
|
|
35
87
|
throttled: bool = False,
|
|
88
|
+
previous: "Discovery|None" = None,
|
|
89
|
+
release_detail: ReleaseDetail | None = None,
|
|
90
|
+
installation_detail: DiscoveryInstallationDetail | None = None,
|
|
91
|
+
current_detail: DiscoveryArtefactDetail | None = None,
|
|
92
|
+
latest_detail: DiscoveryArtefactDetail | None = None,
|
|
36
93
|
) -> None:
|
|
37
94
|
self.provider: ReleaseProvider = provider
|
|
38
95
|
self.source_type: str = provider.source_type
|
|
@@ -42,20 +99,41 @@ class Discovery:
|
|
|
42
99
|
self.entity_picture_url: str | None = entity_picture_url
|
|
43
100
|
self.current_version: str | None = current_version
|
|
44
101
|
self.latest_version: str | None = latest_version
|
|
45
|
-
self.
|
|
102
|
+
self.can_pull: bool = can_pull
|
|
46
103
|
self.can_build: bool = can_build
|
|
47
104
|
self.can_restart: bool = can_restart
|
|
48
|
-
self.release_url: str | None = release_url
|
|
49
|
-
self.release_summary: str | None = release_summary
|
|
50
105
|
self.title_template: str | None = title_template
|
|
51
106
|
self.device_icon: str | None = device_icon
|
|
52
107
|
self.update_type: str | None = update_type
|
|
53
108
|
self.status: str = status
|
|
54
|
-
self.
|
|
55
|
-
self.
|
|
109
|
+
self.publish_policy: PublishPolicy = publish_policy
|
|
110
|
+
self.update_policy: UpdatePolicy = update_policy
|
|
111
|
+
self.version_policy: VersionPolicy = version_policy
|
|
112
|
+
self.version_basis: str | None = version_basis
|
|
113
|
+
self.update_last_attempt: float | None = None
|
|
56
114
|
self.custom: dict[str, Any] = custom or {}
|
|
57
|
-
self.features: list[str] = features or []
|
|
58
115
|
self.throttled: bool = throttled
|
|
116
|
+
self.scan_count: int
|
|
117
|
+
self.first_timestamp: float
|
|
118
|
+
self.last_timestamp: float = time.time()
|
|
119
|
+
self.check_timestamp: float | None = time.time()
|
|
120
|
+
self.release_detail: ReleaseDetail | None = release_detail
|
|
121
|
+
self.current_detail: DiscoveryArtefactDetail | None = current_detail
|
|
122
|
+
self.latest_detail: DiscoveryArtefactDetail | None = latest_detail
|
|
123
|
+
self.installation_detail: DiscoveryInstallationDetail | None = installation_detail
|
|
124
|
+
|
|
125
|
+
if previous:
|
|
126
|
+
self.update_last_attempt = previous.update_last_attempt
|
|
127
|
+
self.first_timestamp = previous.first_timestamp
|
|
128
|
+
self.scan_count = previous.scan_count + 1
|
|
129
|
+
else:
|
|
130
|
+
self.first_timestamp = time.time()
|
|
131
|
+
self.scan_count = 1
|
|
132
|
+
if throttled and previous:
|
|
133
|
+
# roll forward last non-throttled check
|
|
134
|
+
self.check_timestamp = previous.check_timestamp
|
|
135
|
+
elif not throttled:
|
|
136
|
+
self.check_timestamp = time.time()
|
|
59
137
|
|
|
60
138
|
def __repr__(self) -> str:
|
|
61
139
|
"""Build a custom string representation"""
|
|
@@ -70,22 +148,76 @@ class Discovery:
|
|
|
70
148
|
dump = {k: stringify(v) for k, v in self.__dict__.items()}
|
|
71
149
|
return json.dumps(dump)
|
|
72
150
|
|
|
151
|
+
@property
|
|
152
|
+
def can_update(self) -> bool:
|
|
153
|
+
return self.can_pull or self.can_build or self.can_restart
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def features(self) -> list[str]:
|
|
157
|
+
results = []
|
|
158
|
+
if self.can_update:
|
|
159
|
+
# public install-neutral capabilities and Home Assistant features
|
|
160
|
+
results.append("INSTALL")
|
|
161
|
+
results.append("PROGRESS")
|
|
162
|
+
if self.release_detail and self.release_detail.notes_url:
|
|
163
|
+
results.append("RELEASE_NOTES")
|
|
164
|
+
return results
|
|
165
|
+
|
|
73
166
|
@property
|
|
74
167
|
def title(self) -> str:
|
|
75
168
|
if self.title_template:
|
|
76
169
|
return self.title_template.format(discovery=self)
|
|
77
170
|
return self.name
|
|
78
171
|
|
|
172
|
+
def as_dict(self) -> dict[str, str | list | dict | bool | int | None]:
|
|
173
|
+
results: dict[str, str | list | dict | bool | int | None] = {
|
|
174
|
+
"name": self.name,
|
|
175
|
+
"node": self.node,
|
|
176
|
+
"provider": {"source_type": self.provider.source_type},
|
|
177
|
+
"first_scan": {"timestamp": timestamp(self.first_timestamp)},
|
|
178
|
+
"last_scan": {"timestamp": timestamp(self.last_timestamp), "session": self.session, "throttled": self.throttled},
|
|
179
|
+
"scan_count": self.scan_count,
|
|
180
|
+
"installed_version": self.current_version,
|
|
181
|
+
"latest_version": self.latest_version,
|
|
182
|
+
"version_basis": self.version_basis,
|
|
183
|
+
"title": self.title,
|
|
184
|
+
"can_update": self.can_update,
|
|
185
|
+
"can_build": self.can_build,
|
|
186
|
+
"can_restart": self.can_restart,
|
|
187
|
+
"device_icon": self.device_icon,
|
|
188
|
+
"update_type": self.update_type,
|
|
189
|
+
"status": self.status,
|
|
190
|
+
"features": self.features,
|
|
191
|
+
"entity_picture_url": self.entity_picture_url,
|
|
192
|
+
"update_policy": str(self.update_policy),
|
|
193
|
+
"publish_policy": str(self.publish_policy),
|
|
194
|
+
"version_policy": str(self.version_policy),
|
|
195
|
+
"update": {"last_attempt": timestamp(self.update_last_attempt), "in_progress": False},
|
|
196
|
+
"installation_detail": self.installation_detail.as_dict() if self.installation_detail else None,
|
|
197
|
+
"current_detail": self.current_detail.as_dict() if self.current_detail else None,
|
|
198
|
+
"latest_detail": self.latest_detail.as_dict() if self.latest_detail else None,
|
|
199
|
+
}
|
|
200
|
+
if self.release_detail:
|
|
201
|
+
results["release"] = self.release_detail.as_dict() if self.release_detail else None
|
|
202
|
+
if self.custom:
|
|
203
|
+
results[self.source_type] = self.custom
|
|
204
|
+
return results
|
|
205
|
+
|
|
79
206
|
|
|
80
207
|
class ReleaseProvider:
|
|
81
208
|
"""Abstract base class for release providers, such as container scanners or package managers API calls"""
|
|
82
209
|
|
|
83
|
-
def __init__(self, source_type: str = "base") -> None:
|
|
210
|
+
def __init__(self, node_cfg: NodeConfig, source_type: str = "base") -> None:
|
|
84
211
|
self.source_type: str = source_type
|
|
85
212
|
self.discoveries: dict[str, Discovery] = {}
|
|
213
|
+
self.node_cfg: NodeConfig = node_cfg
|
|
86
214
|
self.log: Any = structlog.get_logger().bind(integration=self.source_type)
|
|
87
215
|
self.stopped = Event()
|
|
88
216
|
|
|
217
|
+
def initialize(self) -> None:
|
|
218
|
+
"""Initialize any loops or background tasks, make any startup API calls"""
|
|
219
|
+
pass
|
|
220
|
+
|
|
89
221
|
def stop(self) -> None:
|
|
90
222
|
"""Stop any loops or background tasks"""
|
|
91
223
|
self.log.info("Asking release provider to stop", source_type=self.source_type)
|
|
@@ -106,18 +238,9 @@ class ReleaseProvider:
|
|
|
106
238
|
@abstractmethod
|
|
107
239
|
async def scan(self, session: str) -> AsyncGenerator[Discovery]:
|
|
108
240
|
"""Scan for components to monitor"""
|
|
109
|
-
raise NotImplementedError
|
|
110
241
|
# force recognition as an async generator
|
|
111
|
-
if False:
|
|
112
|
-
yield 0
|
|
113
|
-
|
|
114
|
-
def hass_config_format(self, discovery: Discovery) -> dict:
|
|
115
|
-
_ = discovery
|
|
116
|
-
return {}
|
|
117
|
-
|
|
118
|
-
def hass_state_format(self, discovery: Discovery) -> dict:
|
|
119
|
-
_ = discovery
|
|
120
|
-
return {}
|
|
242
|
+
if False:
|
|
243
|
+
yield 0 # type: ignore[unreachable]
|
|
121
244
|
|
|
122
245
|
@abstractmethod
|
|
123
246
|
def command(self, discovery_name: str, command: str, on_update_start: Callable, on_update_end: Callable) -> bool:
|
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()
|
|
@@ -203,6 +203,7 @@ class MqttPublisher:
|
|
|
203
203
|
async def execute_command(
|
|
204
204
|
self, msg: MQTTMessage | LocalMessage, on_update_start: Callable, on_update_end: Callable
|
|
205
205
|
) -> None:
|
|
206
|
+
# TODO: defer handling of commands where repository is throttled
|
|
206
207
|
logger = self.log.bind(topic=msg.topic, payload=msg.payload)
|
|
207
208
|
comp_name: str | None = None
|
|
208
209
|
command: str | None = None
|
|
@@ -235,7 +236,12 @@ class MqttPublisher:
|
|
|
235
236
|
updated = provider.command(comp_name, command, on_update_start, on_update_end)
|
|
236
237
|
discovery = provider.resolve(comp_name)
|
|
237
238
|
if updated and discovery:
|
|
238
|
-
self.
|
|
239
|
+
if discovery.publish_policy == PublishPolicy.HOMEASSISTANT and self.hass_cfg.discovery.enabled:
|
|
240
|
+
self.publish_hass_config(discovery)
|
|
241
|
+
if discovery.publish_policy in (PublishPolicy.HOMEASSISTANT, PublishPolicy.MQTT):
|
|
242
|
+
self.publish_discovery(discovery)
|
|
243
|
+
if discovery.publish_policy == PublishPolicy.HOMEASSISTANT:
|
|
244
|
+
self.publish_hass_state(discovery)
|
|
239
245
|
else:
|
|
240
246
|
logger.debug("No change to republish after execution")
|
|
241
247
|
logger.info("Execution ended")
|
|
@@ -283,10 +289,12 @@ class MqttPublisher:
|
|
|
283
289
|
|
|
284
290
|
def handle_message(self, msg: mqtt.MQTTMessage | LocalMessage) -> None:
|
|
285
291
|
def update_start(discovery: Discovery) -> None:
|
|
286
|
-
|
|
292
|
+
if discovery.publish_policy in (PublishPolicy.HOMEASSISTANT, PublishPolicy.MQTT):
|
|
293
|
+
self.publish_hass_state(discovery, in_progress=True)
|
|
287
294
|
|
|
288
295
|
def update_end(discovery: Discovery) -> None:
|
|
289
|
-
|
|
296
|
+
if discovery.publish_policy in (PublishPolicy.HOMEASSISTANT, PublishPolicy.MQTT):
|
|
297
|
+
self.publish_hass_state(discovery, in_progress=False)
|
|
290
298
|
|
|
291
299
|
if self.event_loop is not None:
|
|
292
300
|
asyncio.run_coroutine_threadsafe(self.execute_command(msg, update_start, update_end), self.event_loop)
|
|
@@ -298,12 +306,26 @@ class MqttPublisher:
|
|
|
298
306
|
return f"{prefix}/update/{self.node_cfg.name}_{discovery.source_type}_{discovery.name}/update/config"
|
|
299
307
|
|
|
300
308
|
def state_topic(self, discovery: Discovery) -> str:
|
|
309
|
+
return f"{self.cfg.topic_root}/{self.node_cfg.name}/{discovery.source_type}/{discovery.name}/state"
|
|
310
|
+
|
|
311
|
+
def general_topic(self, discovery: Discovery) -> str:
|
|
301
312
|
return f"{self.cfg.topic_root}/{self.node_cfg.name}/{discovery.source_type}/{discovery.name}"
|
|
302
313
|
|
|
303
314
|
def command_topic(self, provider: ReleaseProvider) -> str:
|
|
304
315
|
return f"{self.cfg.topic_root}/{self.node_cfg.name}/{provider.source_type}"
|
|
305
316
|
|
|
317
|
+
def publish_discovery(self, discovery: Discovery, in_progress: bool = False) -> None:
|
|
318
|
+
"""Comprehensive, non Home Assistant specific, base publication"""
|
|
319
|
+
if discovery.publish_policy not in (PublishPolicy.HOMEASSISTANT, PublishPolicy.MQTT):
|
|
320
|
+
return
|
|
321
|
+
self.log.debug("Discovery publish: %s", discovery)
|
|
322
|
+
payload: dict[str, Any] = discovery.as_dict()
|
|
323
|
+
payload["update"]["in_progress"] = in_progress # ty:ignore[invalid-assignment]
|
|
324
|
+
self.publish(self.general_topic(discovery), payload)
|
|
325
|
+
|
|
306
326
|
def publish_hass_state(self, discovery: Discovery, in_progress: bool = False) -> None:
|
|
327
|
+
if discovery.publish_policy != PublishPolicy.HOMEASSISTANT:
|
|
328
|
+
return
|
|
307
329
|
self.log.debug("HASS State update, in progress: %s, discovery: %s", in_progress, discovery)
|
|
308
330
|
self.publish(
|
|
309
331
|
self.state_topic(discovery),
|
|
@@ -315,7 +337,11 @@ class MqttPublisher:
|
|
|
315
337
|
)
|
|
316
338
|
|
|
317
339
|
def publish_hass_config(self, discovery: Discovery) -> None:
|
|
340
|
+
if discovery.publish_policy != PublishPolicy.HOMEASSISTANT:
|
|
341
|
+
return
|
|
318
342
|
object_id = f"{discovery.source_type}_{self.node_cfg.name}_{discovery.name}"
|
|
343
|
+
self.log.debug("HASS Config: %s", object_id)
|
|
344
|
+
|
|
319
345
|
self.publish(
|
|
320
346
|
self.config_topic(discovery),
|
|
321
347
|
hass_format_config(
|
|
@@ -323,10 +349,10 @@ class MqttPublisher:
|
|
|
323
349
|
object_id=object_id,
|
|
324
350
|
area=self.hass_cfg.area,
|
|
325
351
|
state_topic=self.state_topic(discovery),
|
|
352
|
+
attrs_topic=self.general_topic(discovery) if self.hass_cfg.extra_attributes else None,
|
|
326
353
|
command_topic=self.command_topic(discovery.provider),
|
|
327
354
|
force_command_topic=self.hass_cfg.force_command_topic,
|
|
328
355
|
device_creation=self.hass_cfg.device_creation,
|
|
329
|
-
session=discovery.session,
|
|
330
356
|
),
|
|
331
357
|
)
|
|
332
358
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: updates2mqtt
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.7.2
|
|
4
4
|
Summary: System update and docker image notification and execution over MQTT
|
|
5
|
-
Keywords: mqtt,docker,updates,automation,home-assistant,homeassistant,selfhosting
|
|
5
|
+
Keywords: mqtt,docker,oci,container,updates,automation,home-assistant,homeassistant,selfhosting
|
|
6
6
|
Author: jey burrows
|
|
7
7
|
Author-email: jey burrows <jrb@rhizomatics.org.uk>
|
|
8
8
|
License-Expression: Apache-2.0
|
|
@@ -18,21 +18,22 @@ Classifier: Intended Audience :: System Administrators
|
|
|
18
18
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
19
19
|
Classifier: Typing :: Typed
|
|
20
20
|
Classifier: Programming Language :: Python
|
|
21
|
-
Classifier: Programming Language :: Python :: 3.
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
22
|
Requires-Dist: docker>=7.1.0
|
|
23
23
|
Requires-Dist: paho-mqtt>=2.1.0
|
|
24
24
|
Requires-Dist: omegaconf>=2.3.0
|
|
25
25
|
Requires-Dist: structlog>=25.4.0
|
|
26
26
|
Requires-Dist: rich>=14.0.0
|
|
27
27
|
Requires-Dist: httpx>=0.28.1
|
|
28
|
-
Requires-Dist: hishel[httpx]>=
|
|
28
|
+
Requires-Dist: hishel[httpx]>=1.1.0
|
|
29
29
|
Requires-Dist: usingversion>=0.1.2
|
|
30
|
+
Requires-Dist: tzlocal>=5.3.1
|
|
30
31
|
Requires-Python: >=3.13
|
|
31
|
-
Project-URL: Changelog, https://github.com/rhizomatics/updates2mqtt/blob/main/CHANGELOG.md
|
|
32
|
-
Project-URL: Documentation, https://updates2mqtt.rhizomatics.org.uk
|
|
33
32
|
Project-URL: Homepage, https://updates2mqtt.rhizomatics.org.uk
|
|
34
|
-
Project-URL: Issues, https://github.com/rhizomatics/updates2mqtt/issues
|
|
35
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 }
|
|
@@ -60,7 +61,7 @@ Description-Content-Type: text/markdown
|
|
|
60
61
|
|
|
61
62
|
Let Home Assistant tell you about new updates to Docker images for your containers.
|
|
62
63
|
|
|
63
|
-

|
|
64
|
+
{width=300}
|
|
64
65
|
|
|
65
66
|
Read the release notes, and optionally click *Update* to trigger a Docker *pull* (or optionally *build*) and *update*.
|
|
66
67
|
|
|
@@ -71,7 +72,8 @@ Read the release notes, and optionally click *Update* to trigger a Docker *pull*
|
|
|
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
|
-
Currently only Docker containers are supported, either via an image registry check, or a git repo for source (see [Local Builds](local_builds.md))
|
|
75
|
+
Currently only Docker containers are supported, either via an image registry check (using either v1 Docker APIs or the OCI v2 API), or a git repo for source (see [Local Builds](local_builds.md)), with specific handling for Docker, Github Container Registry, Gitlab, Codeberg, Microsoft Container Registry and LinuxServer Registry, with adaptive behaviour to cope with most
|
|
76
|
+
others. 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
77
|
|
|
76
78
|
Components can also be updated, either automatically or triggered via MQTT, for example by hitting the *Install* button in the HomeAssistant update dialog. Icons and release notes can be specified for a better HA experience. See [Home Assistant Integration](home_assistant.md) for details.
|
|
77
79
|
|
|
@@ -80,7 +82,7 @@ To get started, read the [Installation](installation.md) and [Configuration](con
|
|
|
80
82
|
For a quick spin, try this:
|
|
81
83
|
|
|
82
84
|
```bash
|
|
83
|
-
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:
|
|
85
|
+
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:latest
|
|
84
86
|
```
|
|
85
87
|
|
|
86
88
|
or without Docker, using [uv](https://docs.astral.sh/uv/)
|
|
@@ -132,7 +134,8 @@ file, or as `environment` options inside `docker-compose.yaml`.
|
|
|
132
134
|
### Automated updates
|
|
133
135
|
|
|
134
136
|
If Docker containers should be immediately updated, without any confirmation
|
|
135
|
-
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`)
|
|
137
|
+
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
|
|
138
|
+
visible to Home Assistant, then use `Silent`.
|
|
136
139
|
|
|
137
140
|
```yaml title="Example Compose Snippet"
|
|
138
141
|
restarter:
|
|
@@ -142,9 +145,6 @@ restarter:
|
|
|
142
145
|
- UPD2MQTT_UPDATE=AUTO
|
|
143
146
|
```
|
|
144
147
|
|
|
145
|
-
This can be used in conjunction with the `UPD2MQTT_VERSION_INCLUDE` and `UPD2MQTT_VERSION_EXCLUDE` to
|
|
146
|
-
limit which updates get automatically applied, for example excluding nightly builds.
|
|
147
|
-
|
|
148
148
|
Automated updates can also apply to local builds, where a `git_repo_path` has been defined - if there are remote
|
|
149
149
|
commits available to pull, then a `git pull`, `docker compose build` and `docker compose up` will be executed.
|
|
150
150
|
|
|
@@ -159,8 +159,9 @@ The following environment variables can be used to configure containers for `upd
|
|
|
159
159
|
| `UPD2MQTT_RELNOTES` | URL to release notes for the package. | |
|
|
160
160
|
| `UPD2MQTT_GIT_REPO_PATH` | Relative path to a local git repo if the image is built locally. | |
|
|
161
161
|
| `UPD2MQTT_IGNORE` | If set to `True`, the container will be ignored by Updates2MQTT. | False |
|
|
162
|
-
|
|
|
163
|
-
| `
|
|
162
|
+
| |
|
|
163
|
+
| `UPD2MQTT_VERSION_POLICY` | Change how version derived from container label or image hash, `Version`,`Digest`,`Version_Digest` with default of `Auto`|
|
|
164
|
+
| `UPD2MQTT_REGISTRY_TOKEN` | Access token for authentication to container distribution API, as alternative to making a call to `token` service |
|
|
164
165
|
|
|
165
166
|
### Docker Labels
|
|
166
167
|
|
|
@@ -173,8 +174,9 @@ Alternatively, use Docker labels
|
|
|
173
174
|
| `updates2mqtt.relnotes` | `UPD2MQTT_RELNOTES` |
|
|
174
175
|
| `updates2mqtt.git_repo_path` | `UPD2MQTT_GIT_REPO_PATH` |
|
|
175
176
|
| `updates2mqtt.ignore` | `UPD2MQTT_IGNORE` |
|
|
176
|
-
| `updates2mqtt.
|
|
177
|
-
| `updates2mqtt.
|
|
177
|
+
| `updates2mqtt.version_policy` | `UPD2MQTT_VERSION_POLICY` |
|
|
178
|
+
| `updates2mqtt.registry_token` | `UPD2MQTT_REGISTRY_TOKEN` |
|
|
179
|
+
|
|
178
180
|
|
|
179
181
|
|
|
180
182
|
```yaml title="Example Compose Snippet"
|
|
@@ -204,7 +206,7 @@ This component relies on several open source packages:
|
|
|
204
206
|
- [Eclipse Paho](https://eclipse.dev/paho/files/paho.mqtt.python/html/client.html) MQTT client
|
|
205
207
|
- [OmegaConf](https://omegaconf.readthedocs.io) for configuration and validation
|
|
206
208
|
- [structlog](https://www.structlog.org/en/stable/) for structured logging and [rich](https://rich.readthedocs.io/en/stable/) for better exception reporting
|
|
207
|
-
- [hishel](https://hishel.com/
|
|
209
|
+
- [hishel](https://hishel.com/) for caching metadata
|
|
208
210
|
- [httpx](https://www.python-httpx.org) for retrieving metadata
|
|
209
211
|
- The Astral [uv](https://docs.astral.sh/uv/) and [ruff](https://docs.astral.sh/ruff/) tools for development and build
|
|
210
212
|
- [pytest](https://docs.pytest.org/en/stable/) and supporting add-ins for automated testing
|
|
@@ -0,0 +1,18 @@
|
|
|
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/cli.py,sha256=1ntGaJc8rOv8uU5l5oOCs80yey8A--CkHGPFYND0A6U,5237
|
|
5
|
+
updates2mqtt/config.py,sha256=Yfr5tHTVj4Tl-Zpmx6UZ4HBOOFvdoIYXi91bUVgl8E0,7243
|
|
6
|
+
updates2mqtt/hass_formatter.py,sha256=k0aLGg-7wI_C4TixhY-L-iz7n0QCKQ_Pvv37hSp22ww,2779
|
|
7
|
+
updates2mqtt/helpers.py,sha256=fGGBA8JrneAji0AdqyA2waivV9Jq_rXB-CT6TzIFNZ8,9282
|
|
8
|
+
updates2mqtt/integrations/__init__.py,sha256=KmNTUxvVWvqI7rl4I0xZg7XaCmcMS2O4OSv-ClsWM4Q,109
|
|
9
|
+
updates2mqtt/integrations/docker.py,sha256=848AbaNiRGdIRi0nG9-_3JePBHy5d48ETDXilIf_emM,30171
|
|
10
|
+
updates2mqtt/integrations/docker_enrich.py,sha256=1szsw7JpCWQcAG-rRp0KJ3Yj4MgE28C_RCKZngbnPv0,39474
|
|
11
|
+
updates2mqtt/integrations/git_utils.py,sha256=AnMiVW-noaBQ-17FeIl93jwpTSzvr70nIDEcJN3D-gw,4356
|
|
12
|
+
updates2mqtt/model.py,sha256=Pfwy2nSAq6-_ACEhZRsCVXlahTbDd07Ej2o_81SPdxc,10093
|
|
13
|
+
updates2mqtt/mqtt.py,sha256=EYsWKGKzmwf3VIjQmf_oI2C962k3QTWJKXRnLC9kzEU,16687
|
|
14
|
+
updates2mqtt/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
updates2mqtt-1.7.2.dist-info/WHEEL,sha256=XV0cjMrO7zXhVAIyyc8aFf1VjZ33Fen4IiJk5zFlC3g,80
|
|
16
|
+
updates2mqtt-1.7.2.dist-info/entry_points.txt,sha256=qtMKoTPaodbFC3YG7MLElWDjl7CfJdbrxxZyH6Bua8E,83
|
|
17
|
+
updates2mqtt-1.7.2.dist-info/METADATA,sha256=7FEYhqFY1T77JJx54AsJ5GmtJDVydCHQm7g_IiMwraA,12131
|
|
18
|
+
updates2mqtt-1.7.2.dist-info/RECORD,,
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
updates2mqtt/__init__.py,sha256=gnmHrLOSYc-N1-c5VG46OpNpoXEybKzYhEvFMm955P8,237
|
|
2
|
-
updates2mqtt/__main__.py,sha256=HBF00oH5fhS33sI_CdbxNlaUvbIzuuGxwnRYdhHqx0M,194
|
|
3
|
-
updates2mqtt/app.py,sha256=uTgGgZX0xT7ee2-AB7tWTV8zKmtZiIWXHInmP8mrYZs,9357
|
|
4
|
-
updates2mqtt/config.py,sha256=VN594apCjeUAwCekl0ItEMx-4eSeJsZIamHzsKoS0Ns,6136
|
|
5
|
-
updates2mqtt/hass_formatter.py,sha256=6UfBhQLmFEYVVct_JeepIERwNvdwM2z8jkHAv-vOWa0,3191
|
|
6
|
-
updates2mqtt/integrations/__init__.py,sha256=KmNTUxvVWvqI7rl4I0xZg7XaCmcMS2O4OSv-ClsWM4Q,109
|
|
7
|
-
updates2mqtt/integrations/docker.py,sha256=3BQWgAENaoZjmJizrv_yHc3L6GXn8iQuUAXLcrXCS6g,28079
|
|
8
|
-
updates2mqtt/integrations/git_utils.py,sha256=ODCKecWnom1NEKsmDZ2vFmYfnVGtWUgq7svVGcOtSaU,4369
|
|
9
|
-
updates2mqtt/model.py,sha256=YkJyeoEgO1ApqEfcCmeMu9ROk9tOZPYxGx2oBkbXyy0,4657
|
|
10
|
-
updates2mqtt/mqtt.py,sha256=W5kerew922CwaPvaNczVm61bvh3DGJ3k41BNZ1WJuuE,14987
|
|
11
|
-
updates2mqtt/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
-
updates2mqtt-1.6.0.dist-info/WHEEL,sha256=eycQt0QpYmJMLKpE3X9iDk8R04v2ZF0x82ogq-zP6bQ,79
|
|
13
|
-
updates2mqtt-1.6.0.dist-info/entry_points.txt,sha256=Nt1kQQfJ1M2RvcRUnVxe3KCMiX8puHPqz-D7BwqV1L8,55
|
|
14
|
-
updates2mqtt-1.6.0.dist-info/METADATA,sha256=iVHtji5oJMxb05RD5rSoECjIgD_Bk6aa_vGwBQuCoyI,11769
|
|
15
|
-
updates2mqtt-1.6.0.dist-info/RECORD,,
|