updates2mqtt 1.3.7__py3-none-any.whl → 1.4.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
@@ -16,7 +16,7 @@ from updates2mqtt.model import Discovery, ReleaseProvider
16
16
 
17
17
  from .config import Config, load_app_config, load_package_info
18
18
  from .integrations.docker import DockerProvider
19
- from .mqtt import MqttClient
19
+ from .mqtt import MqttPublisher
20
20
 
21
21
  log = structlog.get_logger()
22
22
 
@@ -48,7 +48,7 @@ class App:
48
48
  log.debug("Logging initialized", level=self.cfg.log.level)
49
49
  self.common_pkg = load_package_info(PKG_INFO_FILE)
50
50
 
51
- self.publisher = MqttClient(self.cfg.mqtt, self.cfg.node, self.cfg.homeassistant)
51
+ self.publisher = MqttPublisher(self.cfg.mqtt, self.cfg.node, self.cfg.homeassistant)
52
52
 
53
53
  self.scanners: list[ReleaseProvider] = []
54
54
  self.scan_count: int = 0
@@ -74,7 +74,7 @@ class App:
74
74
  break
75
75
  log.info("Scanning", source=scanner.source_type, session=session)
76
76
  async with asyncio.TaskGroup() as tg:
77
- async for discovery in scanner.scan(session): # type: ignore[attr-defined]
77
+ async for discovery in scanner.scan(session): # xtype: ignore[attr-defined]
78
78
  tg.create_task(self.on_discovery(discovery), name=f"discovery-{discovery.name}")
79
79
  if self.stopped.is_set():
80
80
  break
@@ -83,7 +83,7 @@ class App:
83
83
  log.info("Scan complete", source_type=scanner.source_type)
84
84
  self.last_scan_timestamp = datetime.now(UTC).isoformat()
85
85
 
86
- async def run(self) -> None:
86
+ async def main_loop(self) -> None:
87
87
  log.debug("Starting run loop")
88
88
  self.publisher.start()
89
89
 
@@ -93,7 +93,7 @@ class App:
93
93
  f"Setting up healthcheck every {self.cfg.node.healthcheck.interval} seconds to topic {self.healthcheck_topic}"
94
94
  )
95
95
  self.healthcheck_loop_task = asyncio.create_task(
96
- repeated_call(self.healthcheck, interval=self.cfg.node.healthcheck.interval)
96
+ repeated_call(self.healthcheck, interval=self.cfg.node.healthcheck.interval), name="healthcheck"
97
97
  )
98
98
 
99
99
  for scanner in self.scanners:
@@ -145,7 +145,8 @@ class App:
145
145
  log.info(f"Cancelling {len(running_tasks)} tasks")
146
146
  for t in running_tasks:
147
147
  log.debug("Cancelling task", task=t.get_name())
148
- t.cancel()
148
+ if t.get_name() == "healthcheck" or t.get_name().startswith("discovery-"):
149
+ t.cancel()
149
150
  await asyncio.gather(*running_tasks, return_exceptions=True)
150
151
  log.debug("Cancellation task completed")
151
152
 
@@ -154,7 +155,11 @@ class App:
154
155
  self.stopped.set()
155
156
  for scanner in self.scanners:
156
157
  scanner.stop()
157
- interrupt_task = asyncio.get_event_loop().create_task(self.interrupt_tasks(), eager_start=True) # type: ignore[call-arg] # pyright: ignore[reportCallIssue]
158
+ interrupt_task = asyncio.get_event_loop().create_task(
159
+ self.interrupt_tasks(),
160
+ eager_start=True, # type: ignore[call-arg] # pyright: ignore[reportCallIssue]
161
+ name="interrupt",
162
+ )
158
163
  for t in asyncio.all_tasks():
159
164
  log.debug("Tasks waiting = %s", t)
160
165
  self.publisher.stop()
@@ -168,7 +173,7 @@ class App:
168
173
  self.publisher.publish(
169
174
  topic=self.healthcheck_topic,
170
175
  payload={
171
- "version": updates2mqtt.version,
176
+ "version": updates2mqtt.version, # pyright: ignore[reportAttributeAccessIssue]
172
177
  "node": self.cfg.node.name,
173
178
  "heartbeat_raw": time.time(),
174
179
  "heartbeat_stamp": datetime.now(UTC).isoformat(),
@@ -197,12 +202,12 @@ def run() -> None:
197
202
 
198
203
  from .app import App
199
204
 
200
- log.debug(f"Starting updates2mqtt v{updates2mqtt.version}")
205
+ log.debug(f"Starting updates2mqtt v{updates2mqtt.version}") # pyright: ignore[reportAttributeAccessIssue]
201
206
  app = App()
202
207
 
203
208
  signal.signal(signal.SIGTERM, app.shutdown)
204
209
  try:
205
- asyncio.run(app.run(), debug=False)
210
+ asyncio.run(app.main_loop(), debug=False)
206
211
  log.debug("App exited gracefully")
207
212
  except asyncio.CancelledError:
208
213
  log.debug("App exited on cancelled task")
updates2mqtt/config.py CHANGED
@@ -16,6 +16,7 @@ class MqttConfig:
16
16
  password: str = f"${{oc.env:MQTT_PASS,{MISSING}}}"
17
17
  port: int = "${oc.decode:${oc.env:MQTT_PORT,1883}}" # type: ignore[assignment]
18
18
  topic_root: str = "updates2mqtt"
19
+ protocol: str = "3.11"
19
20
 
20
21
 
21
22
  @dataclass
@@ -135,8 +135,8 @@ class DockerProvider(ReleaseProvider):
135
135
  image_ref = None
136
136
  image_name = None
137
137
  local_versions = None
138
- if c.attrs is None:
139
- logger.warn("No container attributes found, discovery rejected") # type: ignore[unreachable]
138
+ if c.attrs is None or not c.attrs:
139
+ logger.warn("No container attributes found, discovery rejected")
140
140
  return None
141
141
  if c.name is None:
142
142
  logger.warn("No container name found, discovery rejected")
@@ -145,7 +145,7 @@ class DockerProvider(ReleaseProvider):
145
145
  def env_override(env_var: str, default: Any) -> Any | None:
146
146
  return default if c_env.get(env_var) is None else c_env.get(env_var)
147
147
 
148
- env_str = c.attrs["Config"]["Env"]
148
+ env_str = c.attrs.get("Config", {}).get("Env")
149
149
  c_env = dict(env.split("=", maxsplit=1) for env in env_str if "==" not in env)
150
150
  ignore_container: str | None = env_override("UPD2MQTT_IGNORE", "FALSE")
151
151
  if ignore_container and ignore_container.upper() in ("1", "TRUE"):
@@ -247,11 +247,17 @@ class DockerProvider(ReleaseProvider):
247
247
  can_build: bool = self.cfg.allow_build and custom.get("git_repo_path") is not None
248
248
  can_restart: bool = self.cfg.allow_restart and custom.get("compose_path") is not None
249
249
  can_update: bool = False
250
+ if self.cfg.allow_pull and not can_pull and not can_build:
251
+ logger.info(
252
+ f"Pull not available, image_ref:{image_ref},local_version:{local_version},latest_version:{latest_version}"
253
+ )
250
254
  if can_pull or can_build or can_restart:
251
255
  # public install-neutral capabilities and Home Assistant features
252
256
  can_update = True
253
257
  features.append("INSTALL")
254
258
  features.append("PROGRESS")
259
+ elif any((self.cfg.allow_build, self.cfg.allow_restart, self.cfg.allow_pull)):
260
+ logger.info(f"Update not available, can_pull:{can_pull}, can_build:{can_build},can_restart{can_restart}")
255
261
  if relnotes_url:
256
262
  features.append("RELEASE_NOTES")
257
263
  custom["can_pull"] = can_pull
@@ -279,7 +285,7 @@ class DockerProvider(ReleaseProvider):
279
285
  logger.exception("Docker Discovery Failure", container_attrs=c.attrs)
280
286
  return None
281
287
 
282
- async def scan(self, session: str) -> AsyncGenerator[Discovery]: # type: ignore # noqa: PGH003
288
+ async def scan(self, session: str) -> AsyncGenerator[Discovery]:
283
289
  logger = self.log.bind(session=session, action="scan")
284
290
  containers = results = 0
285
291
  for c in self.client.containers.list():
@@ -287,7 +293,7 @@ class DockerProvider(ReleaseProvider):
287
293
  logger.info(f"Shutdown detected, aborting scan at {c}")
288
294
  break
289
295
  containers = containers + 1
290
- result = self.analyze(cast("Container", c), session)
296
+ result = self.analyze(c, session)
291
297
  if result:
292
298
  self.discoveries[result.name] = result
293
299
  results = results + 1
@@ -28,8 +28,10 @@ def git_timestamp(repo_path: Path, git_path: Path) -> datetime.datetime | None:
28
28
  check=True,
29
29
  )
30
30
  return datetime.datetime.fromisoformat(result.stdout.strip())
31
+ except subprocess.CalledProcessError as cpe:
32
+ log.warn("GIT No result from git log at %s: %s", repo_path, cpe)
31
33
  except Exception as e:
32
- log.warn("GIT Unable to parse timestamp at %s - %s: %s", repo_path, result.stdout if result else "<NO RESULT>", e)
34
+ log.error("GIT Unable to parse timestamp at %s - %s: %s", repo_path, result.stdout if result else "<NO RESULT>", e)
33
35
  return None
34
36
 
35
37
 
updates2mqtt/model.py CHANGED
@@ -80,6 +80,10 @@ class ReleaseProvider:
80
80
  @abstractmethod
81
81
  async def scan(self, session: str) -> AsyncGenerator[Discovery]:
82
82
  """Scan for components to monitor"""
83
+ raise NotImplementedError
84
+ # force recognition as an async generator
85
+ if False: # type: ignore[unreachable]
86
+ yield 0
83
87
 
84
88
  def hass_config_format(self, discovery: Discovery) -> dict:
85
89
  _ = discovery
updates2mqtt/mqtt.py CHANGED
@@ -9,8 +9,8 @@ from typing import Any
9
9
  import paho.mqtt.client as mqtt
10
10
  import paho.mqtt.subscribeoptions
11
11
  import structlog
12
- from paho.mqtt.client import MQTTMessage
13
- from paho.mqtt.enums import CallbackAPIVersion, MQTTErrorCode
12
+ from paho.mqtt.client import MQTT_CLEAN_START_FIRST_ONLY, MQTTMessage
13
+ from paho.mqtt.enums import CallbackAPIVersion, MQTTErrorCode, MQTTProtocolVersion
14
14
  from paho.mqtt.properties import Properties
15
15
  from paho.mqtt.reasoncodes import ReasonCode
16
16
 
@@ -28,7 +28,7 @@ class LocalMessage:
28
28
  payload: str | None = field(default=None)
29
29
 
30
30
 
31
- class MqttClient:
31
+ class MqttPublisher:
32
32
  def __init__(self, cfg: MqttConfig, node_cfg: NodeConfig, hass_cfg: HomeAssistantConfig) -> None:
33
33
  self.cfg: MqttConfig = cfg
34
34
  self.node_cfg: NodeConfig = node_cfg
@@ -42,19 +42,39 @@ class MqttClient:
42
42
  def start(self, event_loop: asyncio.AbstractEventLoop | None = None) -> None:
43
43
  logger = self.log.bind(action="start")
44
44
  try:
45
+ protocol: MQTTProtocolVersion
46
+ if self.cfg.protocol in ("3", "3.11"):
47
+ protocol = MQTTProtocolVersion.MQTTv311
48
+ elif self.cfg.protocol == "3.1":
49
+ protocol = MQTTProtocolVersion.MQTTv31
50
+ elif self.cfg.protocol in ("5", "5.0"):
51
+ protocol = MQTTProtocolVersion.MQTTv5
52
+ else:
53
+ self.log.info("No valid MQTT protocol version found (%s), setting to default v3.11", self.cfg.protocol)
54
+ protocol = MQTTProtocolVersion.MQTTv311
55
+ self.log.debug("MQTT protocol set to %r", protocol)
56
+
45
57
  self.event_loop = event_loop or asyncio.get_event_loop()
46
58
  self.client = mqtt.Client(
47
59
  callback_api_version=CallbackAPIVersion.VERSION2,
48
60
  client_id=f"updates2mqtt_{self.node_cfg.name}",
49
- clean_session=True,
61
+ clean_session=True if protocol != MQTTProtocolVersion.MQTTv5 else None,
62
+ protocol=protocol,
50
63
  )
51
64
  self.client.username_pw_set(self.cfg.user, password=self.cfg.password)
52
- rc: MQTTErrorCode = self.client.connect(host=self.cfg.host, port=self.cfg.port, keepalive=60)
65
+ rc: MQTTErrorCode = self.client.connect(
66
+ host=self.cfg.host,
67
+ port=self.cfg.port,
68
+ keepalive=60,
69
+ clean_start=MQTT_CLEAN_START_FIRST_ONLY,
70
+ )
53
71
  self.log.info("Client connection requested", result_code=rc)
54
72
 
55
73
  self.client.on_connect = self.on_connect
56
74
  self.client.on_disconnect = self.on_disconnect
57
75
  self.client.on_message = self.on_message
76
+ self.client.on_subscribe = self.on_subscribe
77
+ self.client.on_unsubscribe = self.on_unsubscribe
58
78
 
59
79
  self.client.loop_start()
60
80
 
@@ -70,7 +90,7 @@ class MqttClient:
70
90
  self.client = None
71
91
 
72
92
  def is_available(self) -> bool:
73
- return not self.fatal_failure.is_set()
93
+ return self.client is not None and not self.fatal_failure.is_set()
74
94
 
75
95
  def on_connect(
76
96
  self, _client: mqtt.Client, _userdata: Any, _flags: mqtt.ConnectFlags, rc: ReasonCode, _props: Properties | None
@@ -83,8 +103,8 @@ class MqttClient:
83
103
  log.error("Invalid MQTT credentials", result_code=rc)
84
104
 
85
105
  self.log.info("Connected to broker", result_code=rc)
86
- for topic in self.providers_by_topic:
87
- self.log.info("(Re)subscribing", topic=topic)
106
+ for topic, provider in self.providers_by_topic.items():
107
+ self.log.info("(Re)subscribing", topic=topic, provider=provider.source_type)
88
108
  self.client.subscribe(topic)
89
109
 
90
110
  def on_disconnect(
@@ -223,12 +243,37 @@ class MqttClient:
223
243
  )
224
244
  self.handle_message(msg)
225
245
 
246
+ def on_subscribe(
247
+ self,
248
+ _client: mqtt.Client,
249
+ userdata: Any,
250
+ mid: int,
251
+ reason_code_list: list[ReasonCode],
252
+ properties: Properties | None = None,
253
+ ) -> None:
254
+ self.log.debug(
255
+ "on_subscribe, userdata=%s, mid=%s, reasons=%s, properties=%s", userdata, mid, reason_code_list, properties
256
+ )
257
+
258
+ def on_unsubscribe(
259
+ self,
260
+ _client: mqtt.Client,
261
+ userdata: Any,
262
+ mid: int,
263
+ reason_code_list: list[ReasonCode],
264
+ properties: Properties | None = None,
265
+ ) -> None:
266
+ self.log.debug(
267
+ "on_unsubscribe, userdata=%s, mid=%s, reasons=%s, properties=%s", userdata, mid, reason_code_list, properties
268
+ )
269
+
226
270
  def on_message(self, _client: mqtt.Client, _userdata: Any, msg: mqtt.MQTTMessage) -> None:
227
271
  """Callback for incoming MQTT messages""" # noqa: D401
228
272
  if msg.topic in self.providers_by_topic:
229
273
  self.handle_message(msg)
230
274
  else:
231
- self.log.warn("Unhandled message: %s", msg.topic)
275
+ # apparently the root non-wildcard sub sometimes brings in child topics
276
+ self.log.debug("Unhandled message #%s on %s:%s", msg.mid, msg.topic, msg.payload)
232
277
 
233
278
  def handle_message(self, msg: mqtt.MQTTMessage | LocalMessage) -> None:
234
279
  def update_start(discovery: Discovery) -> None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: updates2mqtt
3
- Version: 1.3.7
3
+ Version: 1.4.1
4
4
  Summary: System update and docker image notification and execution over MQTT
5
5
  Project-URL: Homepage, https://updates2mqtt.rhizomatics.org.uk
6
6
  Project-URL: Repository, https://github.com/rhizomatics/updates2mqtt
@@ -39,8 +39,12 @@ Description-Content-Type: text/markdown
39
39
 
40
40
  # updates2mqtt
41
41
 
42
+ [![Rhizomatics Open Source](https://img.shields.io/badge/rhizomatics%20open%20source-lightseagreen)](https://github.com/rhizomatics)
43
+
42
44
  [![PyPI - Version](https://img.shields.io/pypi/v/updates2mqtt)](https://pypi.org/project/updates2mqtt/)
43
45
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/rhizomatics/supernotify)
46
+ ![Coverage](https://raw.githubusercontent.com/rhizomatics/updates2mqtt/ff84fbfac358820cf9c9e9e582c063cd1a137116/badges/coverage.svg)
47
+ ![Tests](https://raw.githubusercontent.com/rhizomatics/updates2mqtt/ff84fbfac358820cf9c9e9e582c063cd1a137116/badges/tests.svg)
44
48
  [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/rhizomatics/updates2mqtt/main.svg)](https://results.pre-commit.ci/latest/github/rhizomatics/updates2mqtt/main)
45
49
  [![Publish Python 🐍 distribution 📦 to PyPI and TestPyPI](https://github.com/rhizomatics/updates2mqtt/actions/workflows/pypi-publish.yml/badge.svg)](https://github.com/rhizomatics/updates2mqtt/actions/workflows/pypi-publish.yml)
46
50
  [![Github Deploy](https://github.com/rhizomatics/updates2mqtt/actions/workflows/python-package.yml/badge.svg?branch=main)](https://github.com/rhizomatics/updates2mqtt/actions/workflows/python-package.yml)
@@ -49,9 +53,14 @@ Description-Content-Type: text/markdown
49
53
 
50
54
  ## Summary
51
55
 
52
- Use Home Assistant to notify you of updates to Docker images for your containers and optionally perform the *pull* (or optionally *build*) and *update*.
56
+ Let Home Assistant tell you about new updates to Docker images for your containers.
57
+
58
+ ![Example Home Assistant update page](images/ha_update_detail.png "Home Assistant Updates")
59
+
60
+ Read the release notes, and optionally click *Update* to trigger a Docker *pull* (or optionally *build*) and *update*.
61
+
62
+ ![Example Home Assistant update dialog](images/ha_update_dialog.png "Home Assistant Updates"){width=480}
53
63
 
54
- ![Example Home Assistant update dialog](images/ha_update_detail.png "Home Assistant Updates")
55
64
 
56
65
  ## Description
57
66
 
@@ -59,7 +68,7 @@ updates2mqtt perioidically checks for new versions of components being available
59
68
 
60
69
  Currently only Docker containers are supported, either via an image registry check, or a git repo for source (see [Local Builds](local_builds.md)). The design is modular, so other update sources can be added, at least for notification. The next anticipated is **apt** for Debian based systems.
61
70
 
62
- 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.
71
+ 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.
63
72
 
64
73
  To get started, read the [Installation](installation.md) and [Configuration](configuration.md) pages.
65
74
 
@@ -71,8 +80,7 @@ docker run -e MQTT_USER=user1 -e MQTT_PASS=pass1 -e MQTT_HOST=192.168.1.5 ghcr.i
71
80
 
72
81
  ## Release Support
73
82
 
74
- Presently only Docker containers are supported, although others are planned,
75
- probably with priority for `apt`.
83
+ Presently only Docker containers are supported, although others are planned, probably with priority for `apt`.
76
84
 
77
85
  | Ecosystem | Support | Comments |
78
86
  |-----------|-------------|----------------------------------------------------------------------------------------------------|
@@ -82,31 +90,57 @@ probably with priority for `apt`.
82
90
 
83
91
  A heartbeat JSON payload is optionally published periodically to a configurable MQTT topic, defaulting to `healthcheck/{node_name}/updates2mqtt`. It contains the current version of updates2mqtt, the node name, a timestamp, and some basic stats.
84
92
 
85
- A `healthcheck.sh` script is included in the Docker image, and can be used as a Docker healthcheck, if the container environment variables are set for `MQTT_HOST`, `MQTT_PORT`, `MQTT_USER` and `MQTT_PASS`.
93
+ A `healthcheck.sh` script is included in the Docker image, and can be used as a Docker healthcheck, if the container environment variables are set for `MQTT_HOST`, `MQTT_PORT`, `MQTT_USER` and `MQTT_PASS`. It uses the `mosquitto-clients` Linux package which provides `mosquitto_sub` command to subscribe to topics.
86
94
 
87
- TIP: Check healthcheck is working using `docker inspect --format "{{json .State.Health }}" updates2mqtt | jq`
88
-
89
- ## HomeAssistant integration
95
+ !!! tip
90
96
 
91
- Any updates that have support for automated install will automatically show in the
92
- Home Assistant settings page if the [MQTT Integration](https://www.home-assistant.io/integrations/mqtt/) is installed and automatic discovery is not disabled.
97
+ Check healthcheck is working using `docker inspect --format "{{json .State.Health }}" updates2mqtt | jq`
93
98
 
94
- ![Home Assistant MQTT Integraion configuration](images/ha_mqtt_discovery.png "Home Assistant MQTT Discovery")
99
+ Another approach is using a restarter service directly in Docker Compose to force a restart, in this case once a day:
95
100
 
96
- The `homeassistant` default topic prefix matches the default updates2mqtt config, if its changed in HomeAssistant, then the updates2mqtt config must be changed to match.
101
+ ```yaml title="Example Compose Service"
102
+ restarter:
103
+ image: docker:cli
104
+ volumes: ["/var/run/docker.sock:/var/run/docker.sock"]
105
+ command: ["/bin/sh", "-c", "while true; do sleep 86400; docker restart updates2mqtt; done"]
106
+ restart: unless-stopped
107
+ environment:
108
+ - UPD2MQTT_UPDATE=AUTO
109
+ ```
97
110
 
98
- ![Home Assistant updates in Settings](images/ha_update_page.png "Home Assistant Updates")
111
+ ## Target Containers
99
112
 
100
- For Home Assistant integration, updates2mqtt represents each component being managed as a [MQTT Update](https://www.home-assistant.io/integrations/update.mqtt/) entity, and uses [MQTT discovery(https://www.home-assistant.io/integrations/mqtt/#mqtt-discovery)] so that HomeAssistant automatically picks up components discovered by updates2mqtt with zero configuration on HomeAssistant itself.
113
+ While `updates2mqtt` will discover and monitor all containers running under the Docker daemon,
114
+ there are some options to make to those containers to tune how it works.
101
115
 
102
- There are 3 separate types of MQTT topic used for HomeAssisstant integration:
116
+ These happen by adding environment variables to the containers, typically inside an `.env`
117
+ file, or as `environment` options inside `docker-compose.yaml`.
103
118
 
104
- - *Config* to support auto discovery. A topic is created per component, with a name like `homeassistant/update/dockernuc_docker_jellyfin/update/config`. This can be disabled in the config file, and the `homeassistant` topic prefix can also be configured.
105
- - *State* to report the current version and the latest version available, again one topic per component, like `updates2mqtt/dockernuc/docker/jellyfin`.
106
- - *Command* to support triggering an update. These will be created on the fly by HomeAssistant when an update is requested, and updates2mqtt subscribes to pick up the changes, so you won't typically see these if browsing MQTT topics. Only one is needed per updates2mqtt agent, with a name like `updates2mqtt/dockernuc/docker`
119
+ ### Automated updates
120
+
121
+ If Docker containers should be immediately updated, without any confirmation
122
+ 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`)
123
+
124
+ ```yaml title="Example Compose Snippet"
125
+ restarter:
126
+ image: docker:cli
127
+ command: ["/bin/sh", "-c", "while true; do sleep 86400; docker restart mailserver; done"]
128
+ environment:
129
+ - UPD2MQTT_UPDATE=AUTO
130
+ ```
131
+
132
+ ### Environment Variables
133
+
134
+ The following environment variables can be used to configure containers for `updates2mqtt`:
135
+
136
+ | Env Var | Description | Default |
137
+ |---------| ------------|----------|
138
+ | `UPD2MQTT_UPDATE` | Update mode, either `Passive` or `Auto`. If `Auto`, updates will be installed automatically. | `Passive` |
139
+ | `UPD2MQTT_PICTURE` | URL to an icon to use in Home Assistant. | Docker logo URL |
140
+ | `UPD2MQTT_RELNOTES` | URL to release notes for the package. | |
141
+ | `UPD2MQTT_GIT_REPO_PATH` | Relative path to a local git repo if the image is built locally. | |
142
+ | `UPD2MQTT_IGNORE` | If set to `True`, the container will be ignored by updates2mqtt. | False |
107
143
 
108
- If the package supports automated update, then *Skip* and *Install* buttons will appear on the Home Assistant
109
- interface, and the package can be remotely fetched and the component restarted.
110
144
 
111
145
  ## Related Projects
112
146
 
@@ -114,6 +148,18 @@ Other apps useful for self-hosting with the help of MQTT:
114
148
 
115
149
  - [psmqtt](https://github.com/eschava/psmqtt) - Report system health and metrics via MQTT
116
150
 
151
+ Find more at [awesome-mqtt](https://github.com/rhizomatics/awesome-mqtt)
152
+
117
153
  ## Development
118
154
 
119
- Access to Docker APIs uses the Python [docker-py](https://docker-py.readthedocs.io/en/stable/) SDK for Python. [Eclipse Paho](https://eclipse.dev/paho/files/paho.mqtt.python/html/client.html) is used for MQTT access, and [OmegaConf](https://omegaconf.readthedocs.io) for configuration.
155
+ This component relies on several open source packages:
156
+
157
+ - [docker-py](https://docker-py.readthedocs.io/en/stable/) SDK for Python for access to Docker APIs
158
+ - [Eclipse Paho](https://eclipse.dev/paho/files/paho.mqtt.python/html/client.html) MQTT client
159
+ - [OmegaConf](https://omegaconf.readthedocs.io) for configuration and validation
160
+ - [structlog](https://www.structlog.org/en/stable/) for structured logging and [rich](https://rich.readthedocs.io/en/stable/) for better exception reporting
161
+ - [hishel](https://hishel.com/1.0/) for caching metadata
162
+ - [httpx](https://www.python-httpx.org) for retrieving metadata
163
+ - The Astral [uv](https://docs.astral.sh/uv/) and [ruff](https://docs.astral.sh/ruff/) tools for development and build
164
+ - [pytest](https://docs.pytest.org/en/stable/) and supporting add-ins for automated testing
165
+ - [usingversion](https://pypi.org/project/usingversion/) to log current version info
@@ -0,0 +1,16 @@
1
+ updates2mqtt/__init__.py,sha256=gnmHrLOSYc-N1-c5VG46OpNpoXEybKzYhEvFMm955P8,237
2
+ updates2mqtt/__main__.py,sha256=HBF00oH5fhS33sI_CdbxNlaUvbIzuuGxwnRYdhHqx0M,194
3
+ updates2mqtt/app.py,sha256=-jAe9HcSSRmq5SZCvlsb8bFq2yVBNZQ0alQ5iRjl3tY,8518
4
+ updates2mqtt/config.py,sha256=SK6uhDyUb9C2JYVd0j6KBHzSAfaCFcOUbmmgsq6VSs0,5027
5
+ updates2mqtt/hass_formatter.py,sha256=Ulfj8F0e_1QMmRuJzHsNM2WxHbz9sIkWOWjRq3kQZzs,2772
6
+ updates2mqtt/model.py,sha256=O6GQFhfQvwQDxlZScFHrpQgFNkUrUAeaGGP6AqNua78,3827
7
+ updates2mqtt/mqtt.py,sha256=WiGB2yFj2xUb-5LZh84zTPLilSvHn0CgUCn41FhC5Sk,14664
8
+ updates2mqtt/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ updates2mqtt/integrations/__init__.py,sha256=KmNTUxvVWvqI7rl4I0xZg7XaCmcMS2O4OSv-ClsWM4Q,109
10
+ updates2mqtt/integrations/docker.py,sha256=Yb0XsrRyNZYAkw_ayd2iYLPFwgeA51Lz9HBVAGq3rs4,19273
11
+ updates2mqtt/integrations/git_utils.py,sha256=bPCmQiZpKpMcrGI7xAVmePXHFn8WwjcPNkf7xqDsGQA,2319
12
+ updates2mqtt-1.4.1.dist-info/METADATA,sha256=Q_yunyG90zU0B3uw2U5_FIImF28DHLNcK0otr6tTzVU,9310
13
+ updates2mqtt-1.4.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
14
+ updates2mqtt-1.4.1.dist-info/entry_points.txt,sha256=Hc6NZ2dBevYSUKTJU6NOs8Mw7Vt0S-9lq5FuKb76NCc,54
15
+ updates2mqtt-1.4.1.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
16
+ updates2mqtt-1.4.1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,16 +0,0 @@
1
- updates2mqtt/__init__.py,sha256=gnmHrLOSYc-N1-c5VG46OpNpoXEybKzYhEvFMm955P8,237
2
- updates2mqtt/__main__.py,sha256=HBF00oH5fhS33sI_CdbxNlaUvbIzuuGxwnRYdhHqx0M,194
3
- updates2mqtt/app.py,sha256=7jnmtIkXlX4e4lIt8WCzV19IYrICkA7cU4m9u9QXvRU,8229
4
- updates2mqtt/config.py,sha256=NiaFdMTXXAjZIbtW7LHYjSqu-ONEoouT2uUu506CTtM,5000
5
- updates2mqtt/hass_formatter.py,sha256=Ulfj8F0e_1QMmRuJzHsNM2WxHbz9sIkWOWjRq3kQZzs,2772
6
- updates2mqtt/model.py,sha256=5tWlj3appGGZjkuBeYR2lb-kXoy5mzCn4P_EJQjnwok,3676
7
- updates2mqtt/mqtt.py,sha256=i2l1BlEmnkp3Ie2qeAPVmdhIO1I_mH8Zxbm84cIYtGI,12741
8
- updates2mqtt/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- updates2mqtt/integrations/__init__.py,sha256=KmNTUxvVWvqI7rl4I0xZg7XaCmcMS2O4OSv-ClsWM4Q,109
10
- updates2mqtt/integrations/docker.py,sha256=OX_sXtWVgUJfRSFi_tTBib4gvP_foQI4MoeTkTeLbZc,18868
11
- updates2mqtt/integrations/git_utils.py,sha256=SkAp6XcvCHwaiy17t6F97kcWTjBd7RyEmfhz6M_EhP0,2196
12
- updates2mqtt-1.3.7.dist-info/METADATA,sha256=yFJy-VGcnmHYdFksLx2teBHbuPi-B3J7pObaE5-XVEk,7990
13
- updates2mqtt-1.3.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
14
- updates2mqtt-1.3.7.dist-info/entry_points.txt,sha256=Hc6NZ2dBevYSUKTJU6NOs8Mw7Vt0S-9lq5FuKb76NCc,54
15
- updates2mqtt-1.3.7.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
16
- updates2mqtt-1.3.7.dist-info/RECORD,,