updates2mqtt 1.3.4__py3-none-any.whl → 1.3.6__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
@@ -39,7 +39,8 @@ class App:
39
39
  self.last_scan_timestamp: str | None = None
40
40
  app_config: Config | None = load_app_config(CONF_FILE)
41
41
  if app_config is None:
42
- log.error(f"Invalid configuration at {CONF_FILE}, exiting")
42
+ log.error(f"Invalid configuration at {CONF_FILE}, edit config to fix missing or invalid values and restart")
43
+ log.error("Exiting app")
43
44
  sys.exit(1)
44
45
  self.cfg: Config = app_config
45
46
 
@@ -98,12 +99,17 @@ class App:
98
99
  for scanner in self.scanners:
99
100
  self.publisher.subscribe_hass_command(scanner)
100
101
 
101
- while not self.stopped.is_set():
102
+ while not self.stopped.is_set() and self.publisher.is_available():
102
103
  await self.scan()
103
- if not self.stopped.is_set():
104
+ if not self.stopped.is_set() and self.publisher.is_available():
104
105
  await asyncio.sleep(self.cfg.scan_interval)
105
106
  else:
106
107
  log.info("Stop requested, exiting run loop and skipping sleep")
108
+
109
+ if not self.publisher.is_available():
110
+ log.error("MQTT fatal connection error - check host,port,user,password in config")
111
+ self.shutdown(exit_code=1)
112
+
107
113
  log.debug("Exiting run loop")
108
114
 
109
115
  async def on_discovery(self, discovery: Discovery) -> None:
@@ -143,8 +149,8 @@ class App:
143
149
  await asyncio.gather(*running_tasks, return_exceptions=True)
144
150
  log.debug("Cancellation task completed")
145
151
 
146
- def shutdown(self, *args) -> None: # noqa: ANN002
147
- log.info("Shutting down on SIGTERM: %s", args)
152
+ def shutdown(self, *args, exit_code: int = 143) -> None: # noqa: ANN002, ARG002
153
+ log.info("Shutting down, exit_code: %s", exit_code)
148
154
  self.stopped.set()
149
155
  for scanner in self.scanners:
150
156
  scanner.stop()
@@ -154,8 +160,11 @@ class App:
154
160
  self.publisher.stop()
155
161
  log.debug("Interrupt: %s", interrupt_task.done())
156
162
  log.info("Shutdown handling complete")
163
+ sys.exit(exit_code) # SIGTERM Graceful Exit = 143
157
164
 
158
165
  async def healthcheck(self) -> None:
166
+ if not self.publisher.is_available():
167
+ return
159
168
  self.publisher.publish(
160
169
  topic=self.healthcheck_topic,
161
170
  payload={
@@ -177,7 +186,7 @@ async def repeated_call(func: Callable, interval: int = 60, *args: Any, **kwargs
177
186
  await func(*args, **kwargs)
178
187
  await asyncio.sleep(interval)
179
188
  except asyncio.CancelledError:
180
- log.exception("Periodic task cancelled")
189
+ log.debug("Periodic task cancelled")
181
190
  except Exception:
182
191
  log.exception("Periodic task failed")
183
192
 
updates2mqtt/config.py CHANGED
@@ -4,17 +4,17 @@ from dataclasses import dataclass, field
4
4
  from pathlib import Path
5
5
 
6
6
  import structlog
7
- from omegaconf import MISSING, MissingMandatoryValue, OmegaConf, ValidationError
7
+ from omegaconf import MISSING, DictConfig, MissingMandatoryValue, OmegaConf, ValidationError
8
8
 
9
9
  log = structlog.get_logger()
10
10
 
11
11
 
12
12
  @dataclass
13
13
  class MqttConfig:
14
- host: str = "localhost"
15
- user: str = MISSING
16
- password: str = MISSING
17
- port: int = 1883
14
+ host: str = "${oc.env:MQTT_HOST,localhost}"
15
+ user: str = f"${{oc.env:MQTT_USER,{MISSING}}}"
16
+ password: str = f"${{oc.env:MQTT_PASS,{MISSING}}}"
17
+ port: int = "${oc.decode:${oc.env:MQTT_PORT,1883}}" # type: ignore[assignment]
18
18
  topic_root: str = "updates2mqtt"
19
19
 
20
20
 
@@ -96,6 +96,10 @@ class UpdateInfoConfig:
96
96
  common_packages: dict[str, PackageUpdateInfo] = field(default_factory=lambda: {})
97
97
 
98
98
 
99
+ class IncompleteConfigException(BaseException):
100
+ pass
101
+
102
+
99
103
  def load_package_info(pkginfo_file_path: Path) -> UpdateInfoConfig:
100
104
  if pkginfo_file_path.exists():
101
105
  log.debug("Loading common package update info", path=pkginfo_file_path)
@@ -107,29 +111,40 @@ def load_package_info(pkginfo_file_path: Path) -> UpdateInfoConfig:
107
111
  return typing.cast("UpdateInfoConfig", cfg)
108
112
 
109
113
 
110
- def load_app_config(conf_file_path: Path) -> Config | None:
111
- initializing: bool = False
112
- base_cfg = OmegaConf.structured(Config)
114
+ def load_app_config(conf_file_path: Path, return_invalid: bool = False) -> Config | None:
115
+ base_cfg: DictConfig = OmegaConf.structured(Config)
116
+ is_new: bool = False
113
117
  if conf_file_path.exists():
114
- cfg = OmegaConf.merge(base_cfg, OmegaConf.load(conf_file_path))
118
+ cfg: DictConfig = typing.cast("DictConfig", OmegaConf.merge(base_cfg, OmegaConf.load(conf_file_path)))
115
119
  else:
116
- initializing = True
117
- try:
118
- log.debug("Creating config directory if not already present", path=conf_file_path)
119
- conf_file_path.parent.mkdir(parents=True, exist_ok=True)
120
- except Exception:
121
- log.exception("Unable to create config directory", path=conf_file_path.parent)
120
+ if not conf_file_path.parent.exists():
121
+ try:
122
+ log.debug(f"Creating config directory {conf_file_path.parent} if not already present")
123
+ conf_file_path.parent.mkdir(parents=True, exist_ok=True)
124
+ except Exception:
125
+ log.exception("Unable to create config directory", path=conf_file_path.parent)
122
126
  try:
123
127
  conf_file_path.write_text(OmegaConf.to_yaml(base_cfg))
128
+ log.info(f"Auto-generated a new config file at {conf_file_path}")
129
+ log.info("The config has place holders for MQTT user and password")
130
+ is_new = True
124
131
  except Exception:
125
132
  log.exception("Unable to write config file", path=conf_file_path)
126
133
  cfg = base_cfg
127
134
 
128
135
  try:
129
136
  # Validate that all required fields are present, throw exception now rather than when config first used
130
- OmegaConf.to_container(cfg, throw_on_missing=not initializing)
137
+ OmegaConf.to_container(cfg, throw_on_missing=True)
131
138
  OmegaConf.set_readonly(cfg, True)
132
- return typing.cast("Config", cfg)
139
+ config: Config = typing.cast("Config", cfg)
140
+ if config.mqtt.user == MISSING or config.mqtt.password == MISSING:
141
+ if not is_new:
142
+ log.warning("MQTT connection configuration has place holders")
143
+ if not is_new and not return_invalid:
144
+ return None
145
+ return config
133
146
  except (MissingMandatoryValue, ValidationError) as e:
134
- log.error("Configuration error: %s", e, path=conf_file_path)
135
- return None
147
+ log.error("Configuration error %s", e, path=conf_file_path.as_posix())
148
+ if return_invalid and cfg is not None:
149
+ return typing.cast("Config", cfg)
150
+ raise
updates2mqtt/mqtt.py CHANGED
@@ -3,13 +3,14 @@ import json
3
3
  import time
4
4
  from collections.abc import Callable
5
5
  from dataclasses import dataclass, field
6
+ from threading import Event
6
7
  from typing import Any
7
8
 
8
9
  import paho.mqtt.client as mqtt
9
10
  import paho.mqtt.subscribeoptions
10
11
  import structlog
11
12
  from paho.mqtt.client import MQTTMessage
12
- from paho.mqtt.enums import CallbackAPIVersion
13
+ from paho.mqtt.enums import CallbackAPIVersion, MQTTErrorCode
13
14
  from paho.mqtt.properties import Properties
14
15
  from paho.mqtt.reasoncodes import ReasonCode
15
16
 
@@ -35,6 +36,7 @@ class MqttClient:
35
36
  self.providers_by_topic: dict[str, ReleaseProvider] = {}
36
37
  self.event_loop: asyncio.AbstractEventLoop | None = None
37
38
  self.client: mqtt.Client | None = None
39
+ self.fatal_failure = Event()
38
40
  self.log = structlog.get_logger().bind(host=cfg.host, integration="mqtt")
39
41
 
40
42
  def start(self, event_loop: asyncio.AbstractEventLoop | None = None) -> None:
@@ -47,7 +49,8 @@ class MqttClient:
47
49
  clean_session=True,
48
50
  )
49
51
  self.client.username_pw_set(self.cfg.user, password=self.cfg.password)
50
- self.client.connect(host=self.cfg.host, port=self.cfg.port, keepalive=60)
52
+ rc: MQTTErrorCode = self.client.connect(host=self.cfg.host, port=self.cfg.port, keepalive=60)
53
+ self.log.info("Client connection requested", result_code=rc)
51
54
 
52
55
  self.client.on_connect = self.on_connect
53
56
  self.client.on_disconnect = self.on_disconnect
@@ -66,12 +69,19 @@ class MqttClient:
66
69
  self.client.disconnect()
67
70
  self.client = None
68
71
 
72
+ def is_available(self) -> bool:
73
+ return not self.fatal_failure.is_set()
74
+
69
75
  def on_connect(
70
76
  self, _client: mqtt.Client, _userdata: Any, _flags: mqtt.ConnectFlags, rc: ReasonCode, _props: Properties | None
71
77
  ) -> None:
72
- if not self.client:
73
- self.log.warn("No client, check if started")
78
+ if not self.client or self.fatal_failure.is_set():
79
+ self.log.warn("No client, check if started and authorized")
74
80
  return
81
+ if rc.getName() == "Not authorized":
82
+ self.fatal_failure.set()
83
+ log.error("Invalid MQTT credentials", result_code=rc)
84
+
75
85
  self.log.info("Connected to broker", result_code=rc)
76
86
  for topic in self.providers_by_topic:
77
87
  self.log.info("(Re)subscribing", topic=topic)
@@ -91,6 +101,8 @@ class MqttClient:
91
101
  self, provider: ReleaseProvider, last_scan_session: str | None, wait_time: int = 5, force: bool = False
92
102
  ) -> None:
93
103
  logger = self.log.bind(action="clean")
104
+ if self.fatal_failure.is_set():
105
+ return
94
106
  logger.info("Starting clean cycle")
95
107
  cleaner = mqtt.Client(
96
108
  callback_api_version=CallbackAPIVersion.VERSION1,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: updates2mqtt
3
- Version: 1.3.4
3
+ Version: 1.3.6
4
4
  Summary: System update and docker image notification and execution over MQTT
5
5
  Project-URL: Repository, https://github.com/rhizomatics/updates2mqtt
6
6
  Project-URL: Documentation, https://updates2mqtt.rhizomatics.org.uk
@@ -52,147 +52,25 @@ Use Home Assistant to notify you of updates to Docker images for your containers
52
52
 
53
53
  ## Description
54
54
 
55
- updates2mqtt perioidically checks for new versions of components being available, and publishes new version info to MQTT.
56
- HomeAssistant auto discovery is supported, so all updates can be seen in the same place as Home Assistant's
57
- own components and add-ins.
55
+ 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.
58
56
 
59
- Currently only Docker containers are supported, either via an image registry check, or a git repo for source. The design is modular, so other update sources can be added, at least for notification. The next anticipated is **apt** for Debian based systems.
57
+ 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.
60
58
 
61
- Components can also be updated, either automatically or triggered via MQTT, for example by hitting the *Install*
62
- button in the HomeAssistant update dialog. Icons and release notes can be specified for a better HA experience.
59
+ 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.
63
60
 
64
- ## Install
61
+ To get started, read the [Installation](installation.md) and [Configuration](configuration.md) pages.
65
62
 
66
- updates2mqtt prefers to be run inside a Docker container.
67
-
68
- ### Manual
69
- ```
70
- uv sync
71
- uv run updates2mqtt
72
- ```
73
- ### Docker
74
-
75
- See `examples` directory for a working `docker-compose.yaml`.
76
-
77
- If you want to update and restart containers, then the file system paths to the location of the
78
- directory where the docker compose file lives must be available in the updates2mqtt container.
79
-
80
- The example `docker-compose.yaml` mounts `/home/containers` for this purpose, so if your containers are in
81
- `/home/containers/app1`, `/home/containers/app2` etc, then updates2mqtt will be able to find them. Map as
82
- many root paths as needed.
83
-
84
- ## Configuration
85
-
86
- Create file `config.yaml` in `conf` directory. If the file is not present, a default file will be generated.
87
-
88
- ### Example configuration file
89
-
90
- This is a maximal config file, the minimum is no config file at all, which will generate a default config file. The only mandatory values are the MQTT user name and password, everything else can be omitted.
63
+ For a quick spin, try this:
91
64
 
92
65
  ```yaml
93
-
94
- node:
95
- name: docker-host-1 # Unique name for this instance, used to name MQTT entities. Defaults to O/S hostname
96
- git_repo_path: /usr/bin/git # Path to git inside container, needed only if non-default and using local docker builds
97
- healthcheck:
98
- enabled: true
99
- interval: 300 # publish a heartbeat every 5 minutes
100
- topic_template: healthcheck/{node_name}/updates2mqtt
101
- mqtt:
102
- host: ${oc.env:MQTT_HOST}
103
- user: ${oc.env:MQTT_USER}
104
- password: ${oc.env:MQTT_PASS}$ # Use an environment variable for secrets
105
- port: ${oc.env:MQTT_PORT}
106
- topic_root: updates2mqtt
107
- homeassistant:
108
- discovery:
109
- prefix: homeassistant # Matches the default MQTT discovery prefix in Home Assistant
110
- enabled: true
111
- state_topic_suffix: state
112
- docker:
113
- enabled: true
114
- allow_pull: true # if true, will do a `docker pull` if an update is available
115
- allow_restart: true # if true, will do a `docker-compose up` if an update is installed
116
- allow_build: true # if true, will do a `docker-compose build` if a git repo is configured
117
- compose_version: v2 # Controls whether to use `docker-compose` (v1) or `docker compose` (v2) command
118
- default_entity_picture_url: https://www.docker.com/wp-content/uploads/2022/03/Moby-logo.png # Picture for update dialog
119
- device_icon: mdi:docker # Material Design Icon to use when browsing entities in Home Assistant
120
- # device_icon: mdi:train-car-container # Alternative icon if you don't like Docker branding
121
- discover_metadata:
122
- linuxserver.io:
123
- enabled: true
124
- cache_ttl: 604800 # cache metadata for 1 week
125
- scan_interval: 10800 # sleep interval between scan runs, in seconds
126
- log:
127
- level: INFO
66
+ docker run -e MQTT_USER=user1 -e MQTT_PASS=pass1 -e MQTT_HOST=192.168.1.5 ghcr.io/rhizomatics/updates2mqtt:release
128
67
  ```
129
68
 
130
- ### Moving Secrets Out of Config
131
-
132
- Example use of environment variables, e.g. for secrets:
133
-
134
- ```
135
- mqtt:
136
- password: ${oc.env:MQTT_PASS}
137
- ```
138
- ### Customizing images and release notes
139
-
140
- Individual docker containers can have customized entity pictures or release notes, using env variables, for example in the `docker-compose.yaml` or in a separate `.env` file:
141
-
142
- ```
143
- environment:
144
- - UPD2MQTT_PICTURE=https://frigate.video/images/logo.svg
145
- - UPD2MQTT_RELNOTES=https://github.com/blakeblackshear/frigate/releases
146
- ```
147
-
148
- The images will show up in the *Update* section of *Settings* menu in HomeAssistant,
149
- as will the release notes link. SVG icons should be used.
150
-
151
- Some popular services have the icon and release note links pre-configured, in `common_packages.yaml`,
152
- and packages from `linuxserver.io` can have metadata automatically discovered.
153
-
154
- #### Icon Sources
155
-
156
- - [Homarr Dashboard Icons](https://github.com/homarr-labs/dashboard-icons)
157
- - [Self Hosted Icons](https://github.com/selfhst/icons)
158
- - [Simple Icons](https://github.com/simple-icons/simple-icons)
159
- - [Tabler Icons](https://tabler.io/icons)
160
- - [Papirus Icons](https://github.com/PapirusDevelopmentTeam/papirus-icon-theme)
161
- - [Homelab SVG Assets](https://github.com/loganmarchione/homelab-svg-assets)
162
-
163
- ### Automated updates
164
-
165
- If Docker containers should be immediately updated, without any confirmation
166
- or trigger, *e.g.* from the HomeAssistant update dialog, then set an environment variable `UPD2MQTT_UPDATE`
167
- in the target container to `Auto` ( it defaults to `Passive`)
168
-
169
- ### Custom docker builds
170
-
171
- If the image is locally built from a checked out git repo, package update can be driven
172
- by the availability of git repo changes to pull rather than a new image on a Docker registry.
173
-
174
- Declare the git path using the env var in ``UPD2MQTT_GIT_REPO_PATH`` in the docker container ( directly or via an ``.env`` file).
175
- The git repo at this path will be used as the source of timestamps, and an update command will carry out a
176
- ``git pull`` and ``docker-compose build`` rather than pulling an image.
177
-
178
- Note that the updates2mqtt docker container needs access to this path declared in its volumes, and that has to
179
- be read/write if automated install required.
180
-
181
- ### Environment Variables
182
-
183
- The following environment variables can be used to configure updates2mqtt:
184
-
185
- | Env Var | Description | Default |
186
- |---------| ------------|----------|
187
- | `UPD2MQTT_UPDATE` | Update mode, either `Passive` or `Auto`. If `Auto`, updates will be installed automatically. | `Passive` |
188
- | `UPD2MQTT_PICTURE` | URL to an icon to use in Home Assistant. | Docker logo URL |
189
- | `UPD2MQTT_RELNOTES` | URL to release notes for the package. | |
190
- | `UPD2MQTT_GIT_REPO_PATH` | Relative path to a local git repo if the image is built locally. | |
191
- | `UPD2MQTT_IGNORE` | If set to `True`, the container will be ignored by updates2mqtt. | False |
192
-
193
-
194
69
  ## Release Support
195
70
 
71
+ Presently only Docker containers are supported, although others are planned,
72
+ probably with priority for `apt`.
73
+
196
74
  | Ecosystem | Support | Comments |
197
75
  |-----------|-------------|----------------------------------------------------------------------------------------------------|
198
76
  | Docker | Scan. Fetch | Fetch is ``docker pull`` only. Restart support only for ``docker-compose`` image based containers. |
@@ -1,16 +1,16 @@
1
1
  updates2mqtt/__init__.py,sha256=gnmHrLOSYc-N1-c5VG46OpNpoXEybKzYhEvFMm955P8,237
2
2
  updates2mqtt/__main__.py,sha256=HBF00oH5fhS33sI_CdbxNlaUvbIzuuGxwnRYdhHqx0M,194
3
- updates2mqtt/app.py,sha256=dfoqIIFq0kAm0KHjtEF_RrNC9-fJBQ7Y2_8suwWBOJU,7738
4
- updates2mqtt/config.py,sha256=TIybHWpGAPLA1uWsZt0miwzboX1AlCNHoGO_JoGMlBo,4270
3
+ updates2mqtt/app.py,sha256=7jnmtIkXlX4e4lIt8WCzV19IYrICkA7cU4m9u9QXvRU,8229
4
+ updates2mqtt/config.py,sha256=5nBJyTkmDlgG32jHhi8mMrI4KjWCFl2NJunVYCAyIwQ,5156
5
5
  updates2mqtt/hass_formatter.py,sha256=Ulfj8F0e_1QMmRuJzHsNM2WxHbz9sIkWOWjRq3kQZzs,2772
6
6
  updates2mqtt/model.py,sha256=5tWlj3appGGZjkuBeYR2lb-kXoy5mzCn4P_EJQjnwok,3676
7
- updates2mqtt/mqtt.py,sha256=kDpbq9jk0av6FqyZrSPtg5oQrbklI0BMMH8wxnAp0OI,12230
7
+ updates2mqtt/mqtt.py,sha256=i2l1BlEmnkp3Ie2qeAPVmdhIO1I_mH8Zxbm84cIYtGI,12741
8
8
  updates2mqtt/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  updates2mqtt/integrations/__init__.py,sha256=KmNTUxvVWvqI7rl4I0xZg7XaCmcMS2O4OSv-ClsWM4Q,109
10
10
  updates2mqtt/integrations/docker.py,sha256=OX_sXtWVgUJfRSFi_tTBib4gvP_foQI4MoeTkTeLbZc,18868
11
11
  updates2mqtt/integrations/git_utils.py,sha256=SkAp6XcvCHwaiy17t6F97kcWTjBd7RyEmfhz6M_EhP0,2196
12
- updates2mqtt-1.3.4.dist-info/METADATA,sha256=K96yWjB_b3rp5ErGl__gefFWvcvLSMGMMmDmNlTKQ8I,12781
13
- updates2mqtt-1.3.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
14
- updates2mqtt-1.3.4.dist-info/entry_points.txt,sha256=Hc6NZ2dBevYSUKTJU6NOs8Mw7Vt0S-9lq5FuKb76NCc,54
15
- updates2mqtt-1.3.4.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
16
- updates2mqtt-1.3.4.dist-info/RECORD,,
12
+ updates2mqtt-1.3.6.dist-info/METADATA,sha256=HAwitJE-bHBhD2mAOS_sQJOCPdjU4enU7J2b2OW2vA4,7675
13
+ updates2mqtt-1.3.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
14
+ updates2mqtt-1.3.6.dist-info/entry_points.txt,sha256=Hc6NZ2dBevYSUKTJU6NOs8Mw7Vt0S-9lq5FuKb76NCc,54
15
+ updates2mqtt-1.3.6.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
16
+ updates2mqtt-1.3.6.dist-info/RECORD,,