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 CHANGED
@@ -21,7 +21,6 @@ from .mqtt import MqttPublisher
21
21
  log = structlog.get_logger()
22
22
 
23
23
  CONF_FILE = Path("conf/config.yaml")
24
- PKG_INFO_FILE = Path("./common_packages.yaml")
25
24
  UPDATE_INTERVAL = 60 * 60 * 4
26
25
 
27
26
  # #TODO:
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=manifest=mcr.microsoft.com/dotnet/sdk:latest`
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
- version_policy: VersionPolicy = (
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 casual semver (e.g. v1.030)
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.match(SEMVER_RE, installed.version or "")
624
+ and re.fullmatch(SEMVER_RE, installed.version or "")
602
625
  and latest.version
603
- and re.match(SEMVER_RE, latest.version or "")
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.match(VERSION_RE, installed.version or "")
632
+ and re.fullmatch(VERSION_RE, installed.version or "")
610
633
  and latest.version
611
- and re.match(VERSION_RE, latest.version or "")
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("causualver")
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
- return installed.short_digest, latest.short_digest, basis("digest") # type: ignore[return-value]
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
- UpdateInfoConfig,
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(self.ref, self.image_digest, self.tags, self.attributes, self.annotations, self.version)
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 | None, registry_info: DockerImageInfo | None
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 = OmegaConf.structured(UpdateInfoConfig)
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: dict[str, PackageUpdateInfo] = {
403
- pkg: PackageUpdateInfo(**pkg_cfg) for pkg, pkg_cfg in cfg.common_packages.items() if pkg not in self.pkgs
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.error("Configuration error %s", e, path=PKG_INFO_FILE.as_posix())
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 latest release for %s, found tag %s for name %s",
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 DiscoveryArtefactDetail:
15
- """Provider specific detail"""
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
- class DiscoveryInstallationDetail:
28
+
29
+ class DiscoveryArtefactDetail(DiscoveryDetail):
22
30
  """Provider specific detail"""
23
31
 
24
- @abstractmethod
25
- def as_dict(self) -> dict[str, str | list | dict | bool | int | None]:
26
- return {}
32
+ def __init__(self) -> None:
33
+ super().__init__()
27
34
 
28
35
 
29
- class ReleaseDetail:
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
- if discovery.publish_policy in (PublishPolicy.HOMEASSISTANT, PublishPolicy.MQTT):
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
- if discovery.publish_policy in (PublishPolicy.HOMEASSISTANT, PublishPolicy.MQTT):
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
- asyncio.run_coroutine_threadsafe(self.execute_command(msg, update_start, update_end), self.event_loop)
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.0
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,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.9.27
2
+ Generator: uv 0.9.28
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -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,,