updates2mqtt 1.8.0__py3-none-any.whl → 1.8.1__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 +0 -1
- updates2mqtt/cli.py +1 -1
- updates2mqtt/config.py +7 -5
- updates2mqtt/integrations/docker.py +44 -11
- updates2mqtt/integrations/docker_enrich.py +55 -11
- updates2mqtt/model.py +24 -12
- updates2mqtt/mqtt.py +23 -6
- {updates2mqtt-1.8.0.dist-info → updates2mqtt-1.8.1.dist-info}/METADATA +2 -2
- updates2mqtt-1.8.1.dist-info/RECORD +18 -0
- {updates2mqtt-1.8.0.dist-info → updates2mqtt-1.8.1.dist-info}/WHEEL +1 -1
- updates2mqtt-1.8.0.dist-info/RECORD +0 -18
- {updates2mqtt-1.8.0.dist-info → updates2mqtt-1.8.1.dist-info}/entry_points.txt +0 -0
updates2mqtt/app.py
CHANGED
updates2mqtt/cli.py
CHANGED
|
@@ -29,7 +29,7 @@ Command can be `container`,`tags`,`manifest` or `blob`
|
|
|
29
29
|
* `container=container-name`
|
|
30
30
|
* `container=hash`
|
|
31
31
|
* `tags=ghcr.io/
|
|
32
|
-
* `blob=
|
|
32
|
+
* `blob=mcr.microsoft.com/dotnet/sdk:latest`
|
|
33
33
|
* `tags=quay.io/linuxserver.io/babybuddy`
|
|
34
34
|
* `blob=ghcr.io/blakeblackshear/frigate@sha256:759c36ee869e3e60258350a2e221eae1a4ba1018613e0334f1bc84eb09c4bbbc`
|
|
35
35
|
|
updates2mqtt/config.py
CHANGED
|
@@ -89,11 +89,13 @@ class VersionPolicy(StrEnum):
|
|
|
89
89
|
VERSION = "VERSION"
|
|
90
90
|
DIGEST = "DIGEST"
|
|
91
91
|
VERSION_DIGEST = "VERSION_DIGEST"
|
|
92
|
+
TIMESTAMP = "TIMESTAMP"
|
|
92
93
|
|
|
93
94
|
|
|
94
95
|
@dataclass
|
|
95
96
|
class DockerPackageUpdateInfo:
|
|
96
97
|
image_name: str = MISSING # untagged image ref
|
|
98
|
+
version_policy: VersionPolicy = VersionPolicy.AUTO
|
|
97
99
|
|
|
98
100
|
|
|
99
101
|
@dataclass
|
|
@@ -104,11 +106,6 @@ class PackageUpdateInfo:
|
|
|
104
106
|
source_repo_url: str | None = None
|
|
105
107
|
|
|
106
108
|
|
|
107
|
-
@dataclass
|
|
108
|
-
class UpdateInfoConfig:
|
|
109
|
-
common_packages: dict[str, PackageUpdateInfo] = field(default_factory=lambda: {})
|
|
110
|
-
|
|
111
|
-
|
|
112
109
|
@dataclass
|
|
113
110
|
class DockerConfig:
|
|
114
111
|
enabled: bool = True
|
|
@@ -177,6 +174,11 @@ class Config:
|
|
|
177
174
|
packages: dict[str, PackageUpdateInfo] = field(default_factory=dict)
|
|
178
175
|
|
|
179
176
|
|
|
177
|
+
@dataclass
|
|
178
|
+
class CommonPackages:
|
|
179
|
+
common_packages: dict[str, PackageUpdateInfo] = field(default_factory=dict)
|
|
180
|
+
|
|
181
|
+
|
|
180
182
|
class IncompleteConfigException(BaseException):
|
|
181
183
|
pass
|
|
182
184
|
|
|
@@ -301,9 +301,7 @@ class DockerProvider(ReleaseProvider):
|
|
|
301
301
|
if customization.ignore:
|
|
302
302
|
logger.info("Container ignored due to UPD2MQTT_IGNORE setting")
|
|
303
303
|
return None
|
|
304
|
-
|
|
305
|
-
self.cfg.version_policy if not customization.version_policy else customization.version_policy
|
|
306
|
-
)
|
|
304
|
+
|
|
307
305
|
if customization.update == UpdatePolicy.AUTO:
|
|
308
306
|
logger.debug("Auto update policy detected")
|
|
309
307
|
update_policy: UpdatePolicy = customization.update or UpdatePolicy.PASSIVE
|
|
@@ -313,6 +311,20 @@ class DockerProvider(ReleaseProvider):
|
|
|
313
311
|
local_info, service_info = self.local_info_builder.build_image_info(c)
|
|
314
312
|
pkg_info: PackageUpdateInfo = self.default_metadata(local_info)
|
|
315
313
|
|
|
314
|
+
version_policy: VersionPolicy
|
|
315
|
+
if customization.version_policy:
|
|
316
|
+
logger.debug("Overriding version_policy to local customization: %s", customization.version_policy)
|
|
317
|
+
version_policy = customization.version_policy
|
|
318
|
+
else:
|
|
319
|
+
if self.cfg.version_policy == VersionPolicy.AUTO and pkg_info.docker:
|
|
320
|
+
logger.debug(
|
|
321
|
+
"Version policy, pkg level %s, config level: %s", pkg_info.docker.version_policy, self.cfg.version_policy
|
|
322
|
+
)
|
|
323
|
+
version_policy = pkg_info.docker.version_policy or self.cfg.version_policy
|
|
324
|
+
else:
|
|
325
|
+
logger.debug("Version policy, fixed config level: %s", self.cfg.version_policy)
|
|
326
|
+
version_policy = self.cfg.version_policy
|
|
327
|
+
|
|
316
328
|
try:
|
|
317
329
|
service_info.git_repo_path = customization.git_repo_path
|
|
318
330
|
|
|
@@ -521,7 +533,7 @@ class DockerProvider(ReleaseProvider):
|
|
|
521
533
|
|
|
522
534
|
def default_metadata(self, image_info: DockerImageInfo) -> PackageUpdateInfo:
|
|
523
535
|
for enricher in self.pkg_enrichers:
|
|
524
|
-
pkg_info = enricher.enrich(image_info)
|
|
536
|
+
pkg_info: PackageUpdateInfo | None = enricher.enrich(image_info)
|
|
525
537
|
if pkg_info is not None:
|
|
526
538
|
return pkg_info
|
|
527
539
|
raise ValueError("No enricher could provide metadata, not even default enricher")
|
|
@@ -588,30 +600,41 @@ def select_versions(version_policy: VersionPolicy, installed: DockerImageInfo, l
|
|
|
588
600
|
basis("version-digest"),
|
|
589
601
|
)
|
|
590
602
|
|
|
603
|
+
if (
|
|
604
|
+
version_policy == VersionPolicy.TIMESTAMP
|
|
605
|
+
and installed.created
|
|
606
|
+
and latest.created
|
|
607
|
+
and (
|
|
608
|
+
(latest.created > installed.created and latest.short_digest != installed.short_digest)
|
|
609
|
+
or (latest.created == installed.created and latest.short_digest == installed.short_digest)
|
|
610
|
+
)
|
|
611
|
+
):
|
|
612
|
+
return installed.created, latest.created, basis("timestamp")
|
|
613
|
+
|
|
591
614
|
phase = 1
|
|
592
615
|
if version_policy == VersionPolicy.AUTO and (
|
|
593
616
|
(installed.version == latest.version and installed.short_digest == latest.short_digest)
|
|
594
617
|
or (installed.version != latest.version and installed.short_digest != latest.short_digest)
|
|
595
618
|
):
|
|
596
|
-
# detect semver, or
|
|
619
|
+
# detect semver, or v semver (e.g. v1.030)
|
|
597
620
|
# only use this if both version and digest are consistently agreeing or disagreeing
|
|
598
621
|
# if the strict conditions work, people see nice version numbers on screen rather than hashes
|
|
599
622
|
if (
|
|
600
623
|
installed.version
|
|
601
|
-
and re.
|
|
624
|
+
and re.fullmatch(SEMVER_RE, installed.version or "")
|
|
602
625
|
and latest.version
|
|
603
|
-
and re.
|
|
626
|
+
and re.fullmatch(SEMVER_RE, latest.version or "")
|
|
604
627
|
):
|
|
605
628
|
# Smells like semver, override if not using version_policy
|
|
606
629
|
return installed.version, latest.version, basis("semver")
|
|
607
630
|
if (
|
|
608
631
|
installed.version
|
|
609
|
-
and re.
|
|
632
|
+
and re.fullmatch(VERSION_RE, installed.version or "")
|
|
610
633
|
and latest.version
|
|
611
|
-
and re.
|
|
634
|
+
and re.fullmatch(VERSION_RE, latest.version or "")
|
|
612
635
|
):
|
|
613
636
|
# Smells like casual semver, override if not using version_policy
|
|
614
|
-
return installed.version, latest.version, basis("
|
|
637
|
+
return installed.version, latest.version, basis("casualver")
|
|
615
638
|
|
|
616
639
|
# AUTO or fallback
|
|
617
640
|
phase = 2
|
|
@@ -634,6 +657,15 @@ def select_versions(version_policy: VersionPolicy, installed: DockerImageInfo, l
|
|
|
634
657
|
|
|
635
658
|
# Fall back to digests, image or repo index
|
|
636
659
|
phase = 4
|
|
660
|
+
if (
|
|
661
|
+
installed.created
|
|
662
|
+
and latest.created
|
|
663
|
+
and (
|
|
664
|
+
(latest.created > installed.created and latest.short_digest != installed.short_digest)
|
|
665
|
+
or (latest.created == installed.created and latest.short_digest == installed.short_digest)
|
|
666
|
+
)
|
|
667
|
+
):
|
|
668
|
+
return installed.created, latest.created, basis("timestamp")
|
|
637
669
|
if installed_digest_available and latest_digest_available:
|
|
638
670
|
return installed.short_digest, latest.short_digest, basis("digest") # type: ignore[return-value]
|
|
639
671
|
if installed.version and not latest.version and not latest.short_digest and not latest.repo_digest:
|
|
@@ -660,7 +692,8 @@ def select_versions(version_policy: VersionPolicy, installed: DockerImageInfo, l
|
|
|
660
692
|
return condense_repo_id(latest), condense_repo_id(latest), basis("repo-digest")
|
|
661
693
|
|
|
662
694
|
if installed_digest_available and not latest_digest_available:
|
|
663
|
-
|
|
695
|
+
# no new digest, so latest is the current
|
|
696
|
+
return installed.short_digest, installed.short_digest, basis("digest") # type: ignore[return-value]
|
|
664
697
|
|
|
665
698
|
log.warn("No versions can be determined for %s", installed.ref)
|
|
666
699
|
phase = 999
|
|
@@ -14,6 +14,7 @@ from updates2mqtt.model import DiscoveryArtefactDetail, DiscoveryInstallationDet
|
|
|
14
14
|
|
|
15
15
|
if typing.TYPE_CHECKING:
|
|
16
16
|
from docker.models.images import RegistryData
|
|
17
|
+
from omegaconf.dictconfig import DictConfig
|
|
17
18
|
from http import HTTPStatus
|
|
18
19
|
|
|
19
20
|
import docker
|
|
@@ -21,12 +22,13 @@ import docker.errors
|
|
|
21
22
|
|
|
22
23
|
from updates2mqtt.config import (
|
|
23
24
|
PKG_INFO_FILE,
|
|
25
|
+
CommonPackages,
|
|
24
26
|
DockerConfig,
|
|
25
27
|
DockerPackageUpdateInfo,
|
|
26
28
|
GitHubConfig,
|
|
27
29
|
PackageUpdateInfo,
|
|
28
30
|
RegistryConfig,
|
|
29
|
-
|
|
31
|
+
VersionPolicy,
|
|
30
32
|
)
|
|
31
33
|
|
|
32
34
|
log: Any = structlog.get_logger()
|
|
@@ -103,7 +105,9 @@ class DockerImageInfo(DiscoveryArtefactDetail):
|
|
|
103
105
|
annotations: dict[str, Any] | None = None,
|
|
104
106
|
platform: str | None = None, # test harness simplification
|
|
105
107
|
version: str | None = None, # test harness simplification
|
|
108
|
+
created: str | None = None,
|
|
106
109
|
) -> None:
|
|
110
|
+
super().__init__()
|
|
107
111
|
self.ref: str = ref
|
|
108
112
|
self.version: str | None = version
|
|
109
113
|
self.image_digest: str | None = image_digest
|
|
@@ -125,6 +129,7 @@ class DockerImageInfo(DiscoveryArtefactDetail):
|
|
|
125
129
|
self.error: str | None = None
|
|
126
130
|
self.platform: str | None = platform
|
|
127
131
|
self.custom: dict[str, str | float | int | bool | None] = {}
|
|
132
|
+
self.created: str | None = created
|
|
128
133
|
|
|
129
134
|
self.local_build: bool = not self.repo_digests
|
|
130
135
|
self.index_name, remote_name = resolve_repository_name(ref)
|
|
@@ -215,12 +220,15 @@ class DockerImageInfo(DiscoveryArtefactDetail):
|
|
|
215
220
|
return None
|
|
216
221
|
|
|
217
222
|
def reuse(self) -> "DockerImageInfo":
|
|
218
|
-
cloned = DockerImageInfo(
|
|
223
|
+
cloned = DockerImageInfo(
|
|
224
|
+
self.ref, self.image_digest, self.tags, self.attributes, self.annotations, self.version, self.created
|
|
225
|
+
)
|
|
219
226
|
cloned.origin = "REUSED"
|
|
220
227
|
return cloned
|
|
221
228
|
|
|
222
229
|
def as_dict(self, minimal: bool = True) -> dict[str, str | list | dict | bool | int | None]:
|
|
223
230
|
result: dict[str, str | list | dict | bool | int | None] = {
|
|
231
|
+
"captured": self.captured.isoformat(),
|
|
224
232
|
"image_ref": self.ref,
|
|
225
233
|
"name": self.name,
|
|
226
234
|
"version": self.version,
|
|
@@ -267,7 +275,7 @@ def _select_annotation(
|
|
|
267
275
|
|
|
268
276
|
|
|
269
277
|
def cherrypick_annotations(
|
|
270
|
-
local_info: DockerImageInfo
|
|
278
|
+
local_info: DockerImageInfo, registry_info: DockerImageInfo | None
|
|
271
279
|
) -> dict[str, str | float | int | bool | None]:
|
|
272
280
|
"""https://github.com/opencontainers/image-spec/blob/main/annotations.md"""
|
|
273
281
|
results: dict[str, str | float | int | bool | None] = {}
|
|
@@ -279,11 +287,25 @@ def cherrypick_annotations(
|
|
|
279
287
|
("image_created", "org.opencontainers.image.created"),
|
|
280
288
|
("image_version", "org.opencontainers.image.version"),
|
|
281
289
|
("image_revision", "org.opencontainers.image.revision"),
|
|
290
|
+
("ref_name", "org.opencontainers.image.ref.name"),
|
|
282
291
|
("title", "org.opencontainers.image.title"),
|
|
283
292
|
("vendor", "org.opencontainers.image.vendor"),
|
|
284
293
|
("source", "org.opencontainers.image.source"),
|
|
285
294
|
]:
|
|
286
295
|
results.update(_select_annotation(either_name, either_label, local_info, registry_info))
|
|
296
|
+
if (
|
|
297
|
+
results.get("ref_name") == "ubuntu"
|
|
298
|
+
and local_info.name != "ubuntu"
|
|
299
|
+
and results.get("image_version")
|
|
300
|
+
and re.fullmatch(r"^2\d\.\d\d$", cast("str", results["image_version"]))
|
|
301
|
+
):
|
|
302
|
+
log.debug(
|
|
303
|
+
"Suppressing %s base %s version leaking into image version: %s",
|
|
304
|
+
local_info.name,
|
|
305
|
+
results["ref_name"],
|
|
306
|
+
results["image_version"],
|
|
307
|
+
)
|
|
308
|
+
del results["image_version"]
|
|
287
309
|
return results
|
|
288
310
|
|
|
289
311
|
|
|
@@ -343,8 +365,11 @@ class LocalContainerInfo:
|
|
|
343
365
|
labels: dict[str, str | float | int | bool | None] = cherrypick_annotations(image_info, None)
|
|
344
366
|
# capture container labels/annotations, not image ones
|
|
345
367
|
labels = labels or {}
|
|
368
|
+
if container.image and container.image.attrs:
|
|
369
|
+
image_info.created = container.image.attrs.get("Created")
|
|
346
370
|
image_info.custom = labels
|
|
347
371
|
image_info.version = cast("str|None", labels.get("image_version"))
|
|
372
|
+
|
|
348
373
|
return image_info, service_info
|
|
349
374
|
|
|
350
375
|
|
|
@@ -383,7 +408,7 @@ class DefaultPackageEnricher(PackageEnricher):
|
|
|
383
408
|
def enrich(self, image_info: DockerImageInfo) -> PackageUpdateInfo | None:
|
|
384
409
|
self.log.debug("Default pkg info", image_name=image_info.untagged_ref, image_ref=image_info.ref)
|
|
385
410
|
return PackageUpdateInfo(
|
|
386
|
-
DockerPackageUpdateInfo(image_info.untagged_ref or image_info.ref),
|
|
411
|
+
DockerPackageUpdateInfo(image_info.untagged_ref or image_info.ref, version_policy=VersionPolicy.AUTO),
|
|
387
412
|
logo_url=self.cfg.default_entity_picture_url,
|
|
388
413
|
release_notes_url=None,
|
|
389
414
|
)
|
|
@@ -391,19 +416,25 @@ class DefaultPackageEnricher(PackageEnricher):
|
|
|
391
416
|
|
|
392
417
|
class CommonPackageEnricher(PackageEnricher):
|
|
393
418
|
def initialize(self) -> None:
|
|
419
|
+
base_cfg: DictConfig = OmegaConf.structured(CommonPackages)
|
|
394
420
|
if PKG_INFO_FILE.exists():
|
|
395
421
|
self.log.debug("Loading common package update info", path=PKG_INFO_FILE)
|
|
396
|
-
cfg = OmegaConf.load(PKG_INFO_FILE)
|
|
422
|
+
cfg: DictConfig = typing.cast("DictConfig", OmegaConf.merge(base_cfg, OmegaConf.load(PKG_INFO_FILE)))
|
|
423
|
+
|
|
424
|
+
OmegaConf.to_container(cfg, throw_on_missing=True)
|
|
425
|
+
OmegaConf.set_readonly(cfg, True)
|
|
397
426
|
else:
|
|
398
427
|
self.log.warn("No common package update info found", path=PKG_INFO_FILE)
|
|
399
|
-
cfg =
|
|
428
|
+
cfg = base_cfg
|
|
400
429
|
try:
|
|
430
|
+
common_config: CommonPackages = typing.cast("CommonPackages", cfg)
|
|
401
431
|
# omegaconf broken-ness on optional fields and converting to backclasses
|
|
402
|
-
self.pkgs
|
|
403
|
-
|
|
404
|
-
|
|
432
|
+
self.pkgs = common_config.common_packages
|
|
433
|
+
# self.pkgs: dict[str, PackageUpdateInfo] = {
|
|
434
|
+
# pkg: PackageUpdateInfo(**pkg_cfg) for pkg, pkg_cfg in cfg.common_packages.items() if pkg not in self.pkgs
|
|
435
|
+
# }
|
|
405
436
|
except (MissingMandatoryValue, ValidationError) as e:
|
|
406
|
-
self.log.
|
|
437
|
+
self.log.serror("Configuration error %s", e, path=PKG_INFO_FILE.as_posix())
|
|
407
438
|
raise
|
|
408
439
|
|
|
409
440
|
|
|
@@ -533,10 +564,12 @@ class SourceReleaseEnricher:
|
|
|
533
564
|
api_response = alt_api_response
|
|
534
565
|
elif alt_api_results:
|
|
535
566
|
self.log.debug(
|
|
536
|
-
"Failed to match
|
|
567
|
+
"Failed to match %s release %s on GitHub, found tag %s for name %s published at %s",
|
|
568
|
+
registry_info.name,
|
|
537
569
|
detail.version,
|
|
538
570
|
alt_api_results.get("tag_name"),
|
|
539
571
|
alt_api_results.get("name"),
|
|
572
|
+
alt_api_results.get("published_at"),
|
|
540
573
|
)
|
|
541
574
|
|
|
542
575
|
if api_response and api_response.is_success:
|
|
@@ -545,6 +578,15 @@ class SourceReleaseEnricher:
|
|
|
545
578
|
reactions = api_results.get("reactions") # ty:ignore[possibly-missing-attribute]
|
|
546
579
|
if reactions:
|
|
547
580
|
detail.net_score = reactions.get("+1", 0) - reactions.get("-1", 0)
|
|
581
|
+
elif api_response:
|
|
582
|
+
api_results = httpx_json_content(api_response, default={})
|
|
583
|
+
self.log.debug(
|
|
584
|
+
"Failed to find %s release %s on GitHub, status %s, errors; %s",
|
|
585
|
+
registry_info.name,
|
|
586
|
+
detail.version,
|
|
587
|
+
api_response.status_code,
|
|
588
|
+
api_results.get("errors"),
|
|
589
|
+
)
|
|
548
590
|
else:
|
|
549
591
|
self.log.debug(
|
|
550
592
|
"Failed to fetch GitHub release info",
|
|
@@ -853,6 +895,7 @@ class ContainerDistributionAPIVersionLookup(VersionLookup):
|
|
|
853
895
|
if config and "Labels" in config:
|
|
854
896
|
result.annotations.update(config.get("Labels") or {})
|
|
855
897
|
result.annotations.update(img_config.get("annotations") or {})
|
|
898
|
+
result.created = config.get("created") or config.get("Created")
|
|
856
899
|
else:
|
|
857
900
|
self.log.debug("No config found: %s", manifest)
|
|
858
901
|
except Exception as e:
|
|
@@ -945,5 +988,6 @@ class DockerClientVersionLookup(VersionLookup):
|
|
|
945
988
|
labels: dict[str, str | float | int | bool | None] = cherrypick_annotations(local_image_info, result)
|
|
946
989
|
result.custom = labels or {}
|
|
947
990
|
result.version = cast("str|None", labels.get("image_version"))
|
|
991
|
+
result.created = cast("str|None", labels.get("image_created"))
|
|
948
992
|
result.origin = "DOCKER_CLIENT"
|
|
949
993
|
return result
|
updates2mqtt/model.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import datetime as dt
|
|
1
2
|
import json
|
|
2
3
|
import time
|
|
3
4
|
from abc import abstractmethod
|
|
@@ -6,27 +7,40 @@ from threading import Event
|
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
8
9
|
import structlog
|
|
10
|
+
from tzlocal import get_localzone
|
|
9
11
|
|
|
10
12
|
from updates2mqtt.config import NodeConfig, PublishPolicy, UpdatePolicy, VersionPolicy
|
|
11
13
|
from updates2mqtt.helpers import sanitize_name, timestamp
|
|
12
14
|
|
|
13
15
|
|
|
14
|
-
class
|
|
15
|
-
|
|
16
|
+
class DiscoveryDetail:
|
|
17
|
+
def __init__(self) -> None:
|
|
18
|
+
self.captured: dt.datetime = dt.datetime.now(tz=get_localzone())
|
|
16
19
|
|
|
20
|
+
@abstractmethod
|
|
17
21
|
def as_dict(self) -> dict[str, str | list | dict | bool | int | None]:
|
|
18
22
|
return {}
|
|
19
23
|
|
|
24
|
+
def __str__(self) -> str:
|
|
25
|
+
"""Log friendly"""
|
|
26
|
+
return ",".join(f"{k}:{v}" for k, v in self.as_dict().items())
|
|
20
27
|
|
|
21
|
-
|
|
28
|
+
|
|
29
|
+
class DiscoveryArtefactDetail(DiscoveryDetail):
|
|
22
30
|
"""Provider specific detail"""
|
|
23
31
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return {}
|
|
32
|
+
def __init__(self) -> None:
|
|
33
|
+
super().__init__()
|
|
27
34
|
|
|
28
35
|
|
|
29
|
-
class
|
|
36
|
+
class DiscoveryInstallationDetail(DiscoveryDetail):
|
|
37
|
+
"""Provider specific detail"""
|
|
38
|
+
|
|
39
|
+
def __init__(self) -> None:
|
|
40
|
+
super().__init__()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ReleaseDetail(DiscoveryDetail):
|
|
30
44
|
"""The artefact source details
|
|
31
45
|
|
|
32
46
|
Note this may be an actual software package, or the source details of the wrapping of it
|
|
@@ -34,6 +48,7 @@ class ReleaseDetail:
|
|
|
34
48
|
"""
|
|
35
49
|
|
|
36
50
|
def __init__(self, notes_url: str | None = None, summary: str | None = None) -> None:
|
|
51
|
+
super().__init__()
|
|
37
52
|
self.source_platform: str | None = None
|
|
38
53
|
self.source_repo_url: str | None = None
|
|
39
54
|
self.source_url: str | None = None
|
|
@@ -45,8 +60,9 @@ class ReleaseDetail:
|
|
|
45
60
|
self.summary: str | None = summary
|
|
46
61
|
self.net_score: int | None = None
|
|
47
62
|
|
|
48
|
-
def as_dict(self) -> dict[str, str | None]:
|
|
63
|
+
def as_dict(self) -> dict[str, str | list | dict | bool | int | None]:
|
|
49
64
|
return {
|
|
65
|
+
"captured": self.captured.isoformat(),
|
|
50
66
|
"title": self.title,
|
|
51
67
|
"version": self.version,
|
|
52
68
|
"source_platform": self.source_platform,
|
|
@@ -59,10 +75,6 @@ class ReleaseDetail:
|
|
|
59
75
|
"net_score": str(self.net_score) if self.net_score is not None else None,
|
|
60
76
|
}
|
|
61
77
|
|
|
62
|
-
def __str__(self) -> str:
|
|
63
|
-
"""Log friendly"""
|
|
64
|
-
return ",".join(f"{k}:{v}" for k, v in self.as_dict().items())
|
|
65
|
-
|
|
66
78
|
|
|
67
79
|
class Discovery:
|
|
68
80
|
"""Discovered component from a scan"""
|
updates2mqtt/mqtt.py
CHANGED
|
@@ -10,7 +10,7 @@ from typing import Any
|
|
|
10
10
|
import paho.mqtt.client as mqtt
|
|
11
11
|
import paho.mqtt.subscribeoptions
|
|
12
12
|
import structlog
|
|
13
|
-
from paho.mqtt.client import MQTT_CLEAN_START_FIRST_ONLY, MQTTMessage
|
|
13
|
+
from paho.mqtt.client import MQTT_CLEAN_START_FIRST_ONLY, MQTTMessage, MQTTMessageInfo
|
|
14
14
|
from paho.mqtt.enums import CallbackAPIVersion, MQTTErrorCode, MQTTProtocolVersion
|
|
15
15
|
from paho.mqtt.properties import Properties
|
|
16
16
|
from paho.mqtt.reasoncodes import ReasonCode
|
|
@@ -213,6 +213,7 @@ class MqttPublisher:
|
|
|
213
213
|
payload = msg.payload
|
|
214
214
|
if payload and "|" in payload:
|
|
215
215
|
source_type, comp_name, command = payload.split("|")
|
|
216
|
+
logger.debug("Executing %s:%s:%s", source_type, comp_name, command)
|
|
216
217
|
|
|
217
218
|
provider: ReleaseProvider | None = self.providers_by_topic.get(msg.topic) if msg.topic else None
|
|
218
219
|
if not provider:
|
|
@@ -235,7 +236,7 @@ class MqttPublisher:
|
|
|
235
236
|
self.publish_hass_config(discovery)
|
|
236
237
|
if discovery.publish_policy in (PublishPolicy.HOMEASSISTANT, PublishPolicy.MQTT):
|
|
237
238
|
self.publish_discovery(discovery)
|
|
238
|
-
if discovery.publish_policy == PublishPolicy.HOMEASSISTANT:
|
|
239
|
+
if discovery and discovery.publish_policy == PublishPolicy.HOMEASSISTANT:
|
|
239
240
|
self.publish_hass_state(discovery)
|
|
240
241
|
else:
|
|
241
242
|
logger.debug("No change to republish after execution")
|
|
@@ -284,15 +285,25 @@ class MqttPublisher:
|
|
|
284
285
|
|
|
285
286
|
def handle_message(self, msg: mqtt.MQTTMessage | LocalMessage) -> None:
|
|
286
287
|
def update_start(discovery: Discovery) -> None:
|
|
287
|
-
|
|
288
|
+
self.log.debug("on_update_start: %s", topic=msg.topic)
|
|
289
|
+
if discovery.publish_policy == PublishPolicy.HOMEASSISTANT:
|
|
288
290
|
self.publish_hass_state(discovery, in_progress=True)
|
|
291
|
+
if discovery.publish_policy in (PublishPolicy.HOMEASSISTANT, PublishPolicy.MQTT):
|
|
292
|
+
self.publish_discovery(discovery, in_progress=True)
|
|
289
293
|
|
|
290
294
|
def update_end(discovery: Discovery) -> None:
|
|
291
|
-
|
|
295
|
+
self.log.debug("on_update_end: %s", topic=msg.topic)
|
|
296
|
+
if discovery.publish_policy == PublishPolicy.HOMEASSISTANT:
|
|
292
297
|
self.publish_hass_state(discovery, in_progress=False)
|
|
298
|
+
if discovery.publish_policy in (PublishPolicy.HOMEASSISTANT, PublishPolicy.MQTT):
|
|
299
|
+
self.publish_discovery(discovery, in_progress=False)
|
|
293
300
|
|
|
301
|
+
# TODO: fix double publish on callback and in command exec
|
|
294
302
|
if self.event_loop is not None:
|
|
295
|
-
|
|
303
|
+
self.log.debug("Executing command topic", topic=msg.topic)
|
|
304
|
+
asyncio.run_coroutine_threadsafe(
|
|
305
|
+
self.execute_command(msg=msg, on_update_start=update_start, on_update_end=update_end), loop=self.event_loop
|
|
306
|
+
)
|
|
296
307
|
else:
|
|
297
308
|
self.log.error("No event loop to handle message", topic=msg.topic)
|
|
298
309
|
|
|
@@ -404,4 +415,10 @@ class MqttPublisher:
|
|
|
404
415
|
|
|
405
416
|
def publish(self, topic: str, payload: dict, qos: int = 0, retain: bool = True) -> None:
|
|
406
417
|
if self.client:
|
|
407
|
-
self.client.publish(topic, payload=json.dumps(payload), qos=qos, retain=retain)
|
|
418
|
+
info: MQTTMessageInfo = self.client.publish(topic, payload=json.dumps(payload), qos=qos, retain=retain)
|
|
419
|
+
if info.rc == MQTTErrorCode.MQTT_ERR_SUCCESS:
|
|
420
|
+
self.log.debug("Publish to %s, mid: %s, published: %s, rc: %s", topic, info.mid, info.is_published(), info.rc)
|
|
421
|
+
else:
|
|
422
|
+
self.log.warning("Problem publishing to %s, mid: %s, rc: %s", topic, info.mid, info.rc)
|
|
423
|
+
else:
|
|
424
|
+
self.log.debug("No client to publish at %s", topic)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: updates2mqtt
|
|
3
|
-
Version: 1.8.
|
|
3
|
+
Version: 1.8.1
|
|
4
4
|
Summary: System update and docker image notification and execution over MQTT
|
|
5
5
|
Keywords: mqtt,docker,oci,container,updates,automation,home-assistant,homeassistant,selfhosting
|
|
6
6
|
Author: jey burrows
|
|
@@ -77,7 +77,7 @@ others. The design is modular, so other update sources can be added, at least f
|
|
|
77
77
|
|
|
78
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.
|
|
79
79
|
|
|
80
|
-
To get started, read the [Installation](installation.md) and [Configuration](configuration.md) pages.
|
|
80
|
+
To get started, read the [Installation](installation.md) and [Configuration](configuration/index.md) pages.
|
|
81
81
|
|
|
82
82
|
For a quick spin, try this:
|
|
83
83
|
|
|
@@ -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=FcfYGF5gUO5GMNVECJtRDF3PdJNQG5q7YRkTZGqPuq4,9632
|
|
4
|
+
updates2mqtt/cli.py,sha256=LE9iVS7NldA3nyq44J2RfhL4grwB3X92tvhRWgv0CuU,5820
|
|
5
|
+
updates2mqtt/config.py,sha256=I2td2oUM_IbV7mKvje3EdVYxkYd6oDF8GnebWwz7Y6U,7547
|
|
6
|
+
updates2mqtt/hass_formatter.py,sha256=k0aLGg-7wI_C4TixhY-L-iz7n0QCKQ_Pvv37hSp22ww,2779
|
|
7
|
+
updates2mqtt/helpers.py,sha256=ZT70gL3Hia9BhTas_cTOoDAChD6LMBFYljNX97-lNok,10706
|
|
8
|
+
updates2mqtt/integrations/__init__.py,sha256=KmNTUxvVWvqI7rl4I0xZg7XaCmcMS2O4OSv-ClsWM4Q,109
|
|
9
|
+
updates2mqtt/integrations/docker.py,sha256=eD8GrDBwkHydZmaD1XrLCkOeDBcWaGyM3fh9yeFbbjg,31893
|
|
10
|
+
updates2mqtt/integrations/docker_enrich.py,sha256=X8c4sWgznOIamMEbUwP9iQ5L8IRHW_V0Fn-G5ZWTlTs,45418
|
|
11
|
+
updates2mqtt/integrations/git_utils.py,sha256=AnMiVW-noaBQ-17FeIl93jwpTSzvr70nIDEcJN3D-gw,4356
|
|
12
|
+
updates2mqtt/model.py,sha256=gC-JT3-D7d_qwFmXTbR0PXvDBMX4Ndm_ivToPbn4tmU,10620
|
|
13
|
+
updates2mqtt/mqtt.py,sha256=ZOdfBYWO9vqPSi--Hc5iMeZte3cUtIS2LyrxWpMcCx8,19674
|
|
14
|
+
updates2mqtt/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
updates2mqtt-1.8.1.dist-info/WHEEL,sha256=fAguSjoiATBe7TNBkJwOjyL1Tt4wwiaQGtNtjRPNMQA,80
|
|
16
|
+
updates2mqtt-1.8.1.dist-info/entry_points.txt,sha256=qtMKoTPaodbFC3YG7MLElWDjl7CfJdbrxxZyH6Bua8E,83
|
|
17
|
+
updates2mqtt-1.8.1.dist-info/METADATA,sha256=ky3uQt8qeOzmY2HTN1Y95M6jrVpCSEMbkqtNnVj4arQ,10069
|
|
18
|
+
updates2mqtt-1.8.1.dist-info/RECORD,,
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
updates2mqtt/__init__.py,sha256=gnmHrLOSYc-N1-c5VG46OpNpoXEybKzYhEvFMm955P8,237
|
|
2
|
-
updates2mqtt/__main__.py,sha256=HBF00oH5fhS33sI_CdbxNlaUvbIzuuGxwnRYdhHqx0M,194
|
|
3
|
-
updates2mqtt/app.py,sha256=72zkSm_Md0141XhBtUDW8axAwd-28T8ui7iKhBDCUak,9679
|
|
4
|
-
updates2mqtt/cli.py,sha256=0KlGD75pdHHZgdT34wqLYEbcgMHTtsjfecyVH1xAOy8,5829
|
|
5
|
-
updates2mqtt/config.py,sha256=6b37bYChbZjJ2qba8dzL2hTvWBjH8DZ_MnIughwVZg4,7472
|
|
6
|
-
updates2mqtt/hass_formatter.py,sha256=k0aLGg-7wI_C4TixhY-L-iz7n0QCKQ_Pvv37hSp22ww,2779
|
|
7
|
-
updates2mqtt/helpers.py,sha256=ZT70gL3Hia9BhTas_cTOoDAChD6LMBFYljNX97-lNok,10706
|
|
8
|
-
updates2mqtt/integrations/__init__.py,sha256=KmNTUxvVWvqI7rl4I0xZg7XaCmcMS2O4OSv-ClsWM4Q,109
|
|
9
|
-
updates2mqtt/integrations/docker.py,sha256=WPuNkqvGEKD4R4ar5BF9IDJBERL_ef1JnK9x_V13WNs,30397
|
|
10
|
-
updates2mqtt/integrations/docker_enrich.py,sha256=jXh7w7jrsZyoru5f5dpYYzL-A4gDWRHWSEdxV1IjW7I,43419
|
|
11
|
-
updates2mqtt/integrations/git_utils.py,sha256=AnMiVW-noaBQ-17FeIl93jwpTSzvr70nIDEcJN3D-gw,4356
|
|
12
|
-
updates2mqtt/model.py,sha256=KjbWBXYBlqdsnun2vCeffVqKEAmJjFmukTCvzGIuTzY,10252
|
|
13
|
-
updates2mqtt/mqtt.py,sha256=Lo85HztlHhlE9seb5MuA2zaie_ioR2sJP365WJHFi1o,18533
|
|
14
|
-
updates2mqtt/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
-
updates2mqtt-1.8.0.dist-info/WHEEL,sha256=e_m4S054HL0hyR3CpOk-b7Q7fDX6BuFkgL5OjAExXas,80
|
|
16
|
-
updates2mqtt-1.8.0.dist-info/entry_points.txt,sha256=qtMKoTPaodbFC3YG7MLElWDjl7CfJdbrxxZyH6Bua8E,83
|
|
17
|
-
updates2mqtt-1.8.0.dist-info/METADATA,sha256=2umaqMekQ_p1lI_EIpjEdAZADRbL-PxMFR7c6WvLlSE,10063
|
|
18
|
-
updates2mqtt-1.8.0.dist-info/RECORD,,
|
|
File without changes
|