updates2mqtt 1.7.3__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/mqtt.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import asyncio
2
2
  import json
3
+ import re
3
4
  import time
4
5
  from collections.abc import Callable
5
6
  from dataclasses import dataclass, field
@@ -9,7 +10,7 @@ from typing import Any
9
10
  import paho.mqtt.client as mqtt
10
11
  import paho.mqtt.subscribeoptions
11
12
  import structlog
12
- from paho.mqtt.client import MQTT_CLEAN_START_FIRST_ONLY, MQTTMessage
13
+ from paho.mqtt.client import MQTT_CLEAN_START_FIRST_ONLY, MQTTMessage, MQTTMessageInfo
13
14
  from paho.mqtt.enums import CallbackAPIVersion, MQTTErrorCode, MQTTProtocolVersion
14
15
  from paho.mqtt.properties import Properties
15
16
  from paho.mqtt.reasoncodes import ReasonCode
@@ -21,6 +22,8 @@ from .hass_formatter import hass_format_config, hass_format_state
21
22
 
22
23
  log = structlog.get_logger()
23
24
 
25
+ MQTT_NAME = r"[A-Za-z0-9_\-\.]+"
26
+
24
27
 
25
28
  @dataclass
26
29
  class LocalMessage:
@@ -34,6 +37,7 @@ class MqttPublisher:
34
37
  self.node_cfg: NodeConfig = node_cfg
35
38
  self.hass_cfg: HomeAssistantConfig = hass_cfg
36
39
  self.providers_by_topic: dict[str, ReleaseProvider] = {}
40
+ self.providers_by_type: dict[str, ReleaseProvider] = {}
37
41
  self.event_loop: asyncio.AbstractEventLoop | None = None
38
42
  self.client: mqtt.Client | None = None
39
43
  self.fatal_failure = Event()
@@ -123,69 +127,60 @@ class MqttPublisher:
123
127
  else:
124
128
  self.log.warning("Disconnect failure from broker", result_code=rc)
125
129
 
126
- async def clean_topics(
127
- self, provider: ReleaseProvider, last_scan_session: str | None, wait_time: int = 5, force: bool = False
128
- ) -> None:
130
+ async def clean_topics(self, provider: ReleaseProvider, wait_time: int = 5, max_time: int = 120) -> None:
129
131
  logger = self.log.bind(action="clean")
130
132
  if self.fatal_failure.is_set():
131
133
  return
132
- logger.info("Starting clean cycle")
133
- cleaner = mqtt.Client(
134
- callback_api_version=CallbackAPIVersion.VERSION1,
135
- client_id=f"updates2mqtt_clean_{self.node_cfg.name}",
136
- clean_session=True,
137
- )
138
- results = {"cleaned": 0, "handled": 0, "discovered": 0, "last_timestamp": time.time()}
139
- cleaner.username_pw_set(self.cfg.user, password=self.cfg.password)
140
- cleaner.connect(host=self.cfg.host, port=self.cfg.port, keepalive=60)
141
- prefixes = [
142
- f"{self.hass_cfg.discovery.prefix}/update/{self.node_cfg.name}_{provider.source_type}_",
143
- f"{self.cfg.topic_root}/{self.node_cfg.name}/{provider.source_type}/",
144
- ]
145
-
146
- def cleanup(_client: mqtt.Client, _userdata: Any, msg: mqtt.MQTTMessage) -> None:
147
- if msg.retain and any(msg.topic.startswith(prefix) for prefix in prefixes):
148
- session = None
134
+ try:
135
+ logger.info("Starting clean cycle, max time: %s", max_time)
136
+ cutoff_time: float = time.time() + max_time
137
+ cleaner = mqtt.Client(
138
+ callback_api_version=CallbackAPIVersion.VERSION1,
139
+ client_id=f"updates2mqtt_clean_{self.node_cfg.name}",
140
+ clean_session=True,
141
+ )
142
+ results = {"cleaned": 0, "matched": 0, "discovered": 0, "last_timestamp": time.time()}
143
+ cleaner.username_pw_set(self.cfg.user, password=self.cfg.password)
144
+ cleaner.connect(host=self.cfg.host, port=self.cfg.port, keepalive=60)
145
+
146
+ def cleanup(_client: mqtt.Client, _userdata: Any, msg: mqtt.MQTTMessage) -> None:
147
+ discovery: Discovery | None = None
148
+ if msg.topic.startswith(
149
+ f"{self.hass_cfg.discovery.prefix}/update/{self.node_cfg.name}_{provider.source_type}_"
150
+ ):
151
+ discovery = self.reverse_config_topic(msg.topic, provider.source_type)
152
+ elif msg.topic.startswith(
153
+ f"{self.cfg.topic_root}/{self.node_cfg.name}/{provider.source_type}/"
154
+ ) and msg.topic.endswith("/state"):
155
+ discovery = self.reverse_state_topic(msg.topic, provider.source_type)
156
+ elif msg.topic.startswith(f"{self.cfg.topic_root}/{self.node_cfg.name}/{provider.source_type}/"):
157
+ discovery = self.reverse_general_topic(msg.topic, provider.source_type)
158
+ else:
159
+ logger.debug("Ignoring other topic ", topic=msg.topic)
160
+ return
161
+
149
162
  results["discovered"] += 1
150
- try:
151
- payload = self.safe_json_decode(msg.payload)
152
- session = payload.get("source_session")
153
- except Exception as e:
154
- log.warn(
155
- "Unable to handle payload for %s: %s",
156
- msg.topic,
157
- e,
158
- exc_info=1,
159
- )
160
- results["handled"] += 1
163
+ if discovery is not None:
164
+ results["matched"] += 1
161
165
  results["last_timestamp"] = time.time()
162
- if session is not None and last_scan_session is not None and session != last_scan_session:
163
- log.debug("Removing stale msg", topic=msg.topic, session=session)
166
+ if discovery is None:
167
+ logger.debug("Removing unknown discovery", topic=msg.topic)
164
168
  cleaner.publish(msg.topic, "", retain=True)
165
169
  results["cleaned"] += 1
166
- elif session is None and force:
167
- log.debug("Removing untrackable msg", topic=msg.topic)
168
- cleaner.publish(msg.topic, "", retain=True)
169
- results["cleaned"] += 1
170
- else:
171
- log.debug(
172
- "Retaining topic with current session: %s",
173
- msg.topic,
174
- )
175
- else:
176
- log.debug("Skipping clean of %s", msg.topic)
177
170
 
178
- cleaner.on_message = cleanup
179
- options = paho.mqtt.subscribeoptions.SubscribeOptions(noLocal=True)
180
- cleaner.subscribe(f"{self.hass_cfg.discovery.prefix}/update/#", options=options)
181
- cleaner.subscribe(f"{self.cfg.topic_root}/{self.node_cfg.name}/{provider.source_type}/#", options=options)
171
+ cleaner.on_message = cleanup
172
+ options = paho.mqtt.subscribeoptions.SubscribeOptions(noLocal=True)
173
+ cleaner.subscribe(f"{self.hass_cfg.discovery.prefix}/update/#", options=options)
174
+ cleaner.subscribe(f"{self.cfg.topic_root}/{self.node_cfg.name}/{provider.source_type}/#", options=options)
182
175
 
183
- while time.time() - results["last_timestamp"] <= wait_time:
184
- cleaner.loop(0.5)
176
+ while time.time() - results["last_timestamp"] <= wait_time and time.time() <= cutoff_time:
177
+ cleaner.loop(0.5)
185
178
 
186
- log.info(
187
- f"Clean completed, discovered:{results['discovered']}, handled:{results['handled']}, cleaned:{results['cleaned']}"
188
- )
179
+ logger.info(
180
+ f"Cleaned - discovered:{results['discovered']}, matched:{results['matched']}, cleaned:{results['cleaned']}"
181
+ )
182
+ except Exception as e:
183
+ logger.error("Cleaning topics of stale entries failed: %s", e)
189
184
 
190
185
  def safe_json_decode(self, jsonish: str | bytes | None) -> dict:
191
186
  if jsonish is None:
@@ -218,6 +213,7 @@ class MqttPublisher:
218
213
  payload = msg.payload
219
214
  if payload and "|" in payload:
220
215
  source_type, comp_name, command = payload.split("|")
216
+ logger.debug("Executing %s:%s:%s", source_type, comp_name, command)
221
217
 
222
218
  provider: ReleaseProvider | None = self.providers_by_topic.get(msg.topic) if msg.topic else None
223
219
  if not provider:
@@ -233,14 +229,14 @@ class MqttPublisher:
233
229
  source_type,
234
230
  comp_name,
235
231
  )
236
- updated = provider.command(comp_name, command, on_update_start, on_update_end)
232
+ updated: bool = provider.command(comp_name, command, on_update_start, on_update_end)
237
233
  discovery = provider.resolve(comp_name)
238
234
  if updated and discovery:
239
235
  if discovery.publish_policy == PublishPolicy.HOMEASSISTANT and self.hass_cfg.discovery.enabled:
240
236
  self.publish_hass_config(discovery)
241
237
  if discovery.publish_policy in (PublishPolicy.HOMEASSISTANT, PublishPolicy.MQTT):
242
238
  self.publish_discovery(discovery)
243
- if discovery.publish_policy == PublishPolicy.HOMEASSISTANT:
239
+ if discovery and discovery.publish_policy == PublishPolicy.HOMEASSISTANT:
244
240
  self.publish_hass_state(discovery)
245
241
  else:
246
242
  logger.debug("No change to republish after execution")
@@ -289,15 +285,25 @@ class MqttPublisher:
289
285
 
290
286
  def handle_message(self, msg: mqtt.MQTTMessage | LocalMessage) -> None:
291
287
  def update_start(discovery: Discovery) -> None:
292
- 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:
293
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)
294
293
 
295
294
  def update_end(discovery: Discovery) -> None:
296
- 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:
297
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)
298
300
 
301
+ # TODO: fix double publish on callback and in command exec
299
302
  if self.event_loop is not None:
300
- 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
+ )
301
307
  else:
302
308
  self.log.error("No event loop to handle message", topic=msg.topic)
303
309
 
@@ -305,12 +311,48 @@ class MqttPublisher:
305
311
  prefix = self.hass_cfg.discovery.prefix
306
312
  return f"{prefix}/update/{self.node_cfg.name}_{discovery.source_type}_{discovery.name}/update/config"
307
313
 
314
+ def reverse_config_topic(self, topic: str, source_type: str) -> Discovery | None:
315
+ match = re.fullmatch(
316
+ f"{self.hass_cfg.discovery.prefix}/update/{self.node_cfg.name}_{source_type}_({MQTT_NAME})/update/config",
317
+ topic,
318
+ )
319
+ if match and len(match.groups()) == 1:
320
+ discovery_name: str = match.group(1)
321
+ if discovery_name in self.providers_by_type[source_type].discoveries:
322
+ return self.providers_by_type[source_type].discoveries[discovery_name]
323
+
324
+ self.log.debug("MQTT CONFIG no match for %s", topic)
325
+ return None
326
+
308
327
  def state_topic(self, discovery: Discovery) -> str:
309
328
  return f"{self.cfg.topic_root}/{self.node_cfg.name}/{discovery.source_type}/{discovery.name}/state"
310
329
 
330
+ def reverse_state_topic(self, topic: str, source_type: str) -> Discovery | None:
331
+ match = re.fullmatch(
332
+ f"{self.cfg.topic_root}/{self.node_cfg.name}/{source_type}/({MQTT_NAME})/state",
333
+ topic,
334
+ )
335
+ if match and len(match.groups()) == 1:
336
+ discovery_name: str = match.group(1)
337
+ if discovery_name in self.providers_by_type[source_type].discoveries:
338
+ return self.providers_by_type[source_type].discoveries[discovery_name]
339
+
340
+ self.log.debug("MQTT STATE no match for %s", topic)
341
+ return None
342
+
311
343
  def general_topic(self, discovery: Discovery) -> str:
312
344
  return f"{self.cfg.topic_root}/{self.node_cfg.name}/{discovery.source_type}/{discovery.name}"
313
345
 
346
+ def reverse_general_topic(self, topic: str, source_type: str) -> Discovery | None:
347
+ match = re.fullmatch(f"{self.cfg.topic_root}/{self.node_cfg.name}/{source_type}/({MQTT_NAME})", topic)
348
+ if match and len(match.groups()) == 1:
349
+ discovery_name: str = match.group(1)
350
+ if discovery_name in self.providers_by_type[source_type].discoveries:
351
+ return self.providers_by_type[source_type].discoveries[discovery_name]
352
+
353
+ self.log.debug("MQTT ATTR no match for %s", topic)
354
+ return None
355
+
314
356
  def command_topic(self, provider: ReleaseProvider) -> str:
315
357
  return f"{self.cfg.topic_root}/{self.node_cfg.name}/{provider.source_type}"
316
358
 
@@ -363,6 +405,7 @@ class MqttPublisher:
363
405
  else:
364
406
  self.log.info("Handler subscribing", topic=topic)
365
407
  self.providers_by_topic[topic] = provider
408
+ self.providers_by_type[provider.source_type] = provider
366
409
  self.client.subscribe(topic)
367
410
  return topic
368
411
 
@@ -372,4 +415,10 @@ class MqttPublisher:
372
415
 
373
416
  def publish(self, topic: str, payload: dict, qos: int = 0, retain: bool = True) -> None:
374
417
  if self.client:
375
- 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.7.3
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
@@ -72,12 +72,12 @@ Read the release notes, and optionally click *Update* to trigger a Docker *pull*
72
72
 
73
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.
74
74
 
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
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, Quay and LinuxServer Registry, with adaptive behaviour to cope with most
76
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.
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
 
@@ -91,6 +91,9 @@ or without Docker, using [uv](https://docs.astral.sh/uv/)
91
91
  export MQTT_HOST=192.168.1.1;export MQTT_USER=user1;export MQTT_PASS=user1;uv run --with updates2mqtt python -m updates2mqtt
92
92
  ```
93
93
 
94
+ It also comes with a basic command line tool that will perform the analysis for a single running container, or fetch
95
+ manifests, JSON blobs and lists of tags from remote registries (known to work with GitHub, GitLab, Codeberg, Quay, LSCR and Microsoft MCR).
96
+
94
97
  ## Release Support
95
98
 
96
99
  Presently only Docker containers are supported, although others are planned, probably with priority for `apt`.
@@ -128,7 +131,7 @@ restarter:
128
131
  While `updates2mqtt` will discover and monitor all containers running under the Docker daemon,
129
132
  there are some options to make to those containers to tune how it works.
130
133
 
131
- These happen by adding environment variables to the containers, typically inside an `.env`
134
+ These happen by adding environment variables or docker labels to the containers, typically inside an `.env`
132
135
  file, or as `environment` options inside `docker-compose.yaml`.
133
136
 
134
137
  ### Automated updates
@@ -148,45 +151,6 @@ restarter:
148
151
  Automated updates can also apply to local builds, where a `git_repo_path` has been defined - if there are remote
149
152
  commits available to pull, then a `git pull`, `docker compose build` and `docker compose up` will be executed.
150
153
 
151
- ### Environment Variables
152
-
153
- The following environment variables can be used to configure containers for `updates2mqtt`:
154
-
155
- | Env Var | Description | Default |
156
- |----------------------------|----------------------------------------------------------------------------------------------|-----------------|
157
- | `UPD2MQTT_UPDATE` | Update mode, either `Passive` or `Auto`. If `Auto`, updates will be installed automatically. | `Passive` |
158
- | `UPD2MQTT_PICTURE` | URL to an icon to use in Home Assistant. | Docker logo URL |
159
- | `UPD2MQTT_RELNOTES` | URL to release notes for the package. | |
160
- | `UPD2MQTT_GIT_REPO_PATH` | Relative path to a local git repo if the image is built locally. | |
161
- | `UPD2MQTT_IGNORE` | If set to `True`, the container will be ignored by Updates2MQTT. | False |
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 |
165
-
166
- ### Docker Labels
167
-
168
- Alternatively, use Docker labels
169
-
170
- | Label | Env Var |
171
- |--------------------------------|----------------------------|
172
- | `updates2mqtt.update` | `UPD2MQTT_UPDATE` |
173
- | `updates2mqtt.picture` | `UPD2MQTT_PCITURE` |
174
- | `updates2mqtt.relnotes` | `UPD2MQTT_RELNOTES` |
175
- | `updates2mqtt.git_repo_path` | `UPD2MQTT_GIT_REPO_PATH` |
176
- | `updates2mqtt.ignore` | `UPD2MQTT_IGNORE` |
177
- | `updates2mqtt.version_policy` | `UPD2MQTT_VERSION_POLICY` |
178
- | `updates2mqtt.registry_token` | `UPD2MQTT_REGISTRY_TOKEN` |
179
-
180
-
181
-
182
- ```yaml title="Example Compose Snippet"
183
- restarter:
184
- image: docker:cli
185
- command: ["/bin/sh", "-c", "while true; do sleep 86400; docker restart mailserver; done"]
186
- labels:
187
- updates2mqtt.relnotes: https://component.my.com/release_notes
188
- ```
189
-
190
154
 
191
155
  ## Related Projects
192
156
 
@@ -196,7 +160,7 @@ Other apps useful for self-hosting with the help of MQTT:
196
160
 
197
161
  Find more at [awesome-mqtt](https://github.com/rhizomatics/awesome-mqtt)
198
162
 
199
- For a more powerful Docker update manager, try [What's Up Docker](https://getwud.github.io/wud/)
163
+ For a more powerful Docker focussed update manager, try [What's Up Docker](https://getwud.github.io/wud/)
200
164
 
201
165
  ## Development
202
166
 
@@ -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.26
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=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=mEFTTIQjSFml58Ek8cDRIHSz2JGJyNNT9TECBsqdp6o,9272
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.3.dist-info/WHEEL,sha256=XV0cjMrO7zXhVAIyyc8aFf1VjZ33Fen4IiJk5zFlC3g,80
16
- updates2mqtt-1.7.3.dist-info/entry_points.txt,sha256=qtMKoTPaodbFC3YG7MLElWDjl7CfJdbrxxZyH6Bua8E,83
17
- updates2mqtt-1.7.3.dist-info/METADATA,sha256=LZ_ncnPQG9qVKChVIewF5YmNDRdqCzceR2MPunL2Zno,12131
18
- updates2mqtt-1.7.3.dist-info/RECORD,,