updates2mqtt 1.4.0__py3-none-any.whl → 1.4.2__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
@@ -49,6 +49,8 @@ class HomeAssistantDiscoveryConfig:
49
49
  class HomeAssistantConfig:
50
50
  discovery: HomeAssistantDiscoveryConfig = field(default_factory=HomeAssistantDiscoveryConfig)
51
51
  state_topic_suffix: str = "state"
52
+ device_creation: bool = True
53
+ area: str | None = None
52
54
 
53
55
 
54
56
  @dataclass
@@ -74,7 +76,7 @@ class LogConfig:
74
76
  class Config:
75
77
  log: LogConfig = field(default_factory=LogConfig)
76
78
  node: NodeConfig = field(default_factory=NodeConfig)
77
- mqtt: MqttConfig = field(default_factory=MqttConfig)
79
+ mqtt: MqttConfig = field(default_factory=MqttConfig) # pyright: ignore[reportCallIssue, reportArgumentType]
78
80
  homeassistant: HomeAssistantConfig = field(default_factory=HomeAssistantConfig)
79
81
  docker: DockerConfig = field(default_factory=DockerConfig)
80
82
  scan_interval: int = 60 * 60 * 3
@@ -19,9 +19,16 @@ HASS_UPDATE_SCHEMA = [
19
19
 
20
20
 
21
21
  def hass_format_config(
22
- discovery: Discovery, object_id: str, node_name: str, state_topic: str, command_topic: str | None, session: str
22
+ discovery: Discovery,
23
+ object_id: str,
24
+ node_name: str,
25
+ state_topic: str,
26
+ command_topic: str | None,
27
+ device_creation: bool = True,
28
+ area: str | None = None,
29
+ session: str | None = None,
23
30
  ) -> dict[str, Any]:
24
- config = {
31
+ config: dict[str, Any] = {
25
32
  "name": f"{discovery.name} {discovery.source_type} on {node_name}",
26
33
  "device_class": None, # not firmware, so defaults to null
27
34
  "unique_id": object_id,
@@ -37,11 +44,20 @@ def hass_format_config(
37
44
  "latest_version_topic": state_topic,
38
45
  "latest_version_template": "{{value_json.latest_version}}",
39
46
  "origin": {
40
- "name": "updates2mqtt",
41
- "sw_version": updates2mqtt.version,
47
+ "name": f"{node_name} updates2mqtt Agent",
48
+ "sw_version": updates2mqtt.version, # pyright: ignore[reportAttributeAccessIssue]
42
49
  "support_url": "https://github.com/rhizomatics/updates2mqtt/issues",
43
50
  },
44
51
  }
52
+ if device_creation:
53
+ config["device"] = {
54
+ "name": f"{node_name} updates2mqtt Agent",
55
+ "sw_version": updates2mqtt.version, # pyright: ignore[reportAttributeAccessIssue]
56
+ "manufacturer": "rhizomatics",
57
+ "identifiers": [f"{node_name}.updates2mqtt"],
58
+ }
59
+ if area:
60
+ config["device"]["suggested_area"] = area
45
61
  if command_topic:
46
62
  config["command_topic"] = command_topic
47
63
  config["payload_install"] = f"{discovery.source_type}|{discovery.name}|install"
@@ -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():
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
@@ -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
@@ -90,7 +90,7 @@ class MqttClient:
90
90
  self.client = None
91
91
 
92
92
  def is_available(self) -> bool:
93
- return not self.fatal_failure.is_set()
93
+ return self.client is not None and not self.fatal_failure.is_set()
94
94
 
95
95
  def on_connect(
96
96
  self, _client: mqtt.Client, _userdata: Any, _flags: mqtt.ConnectFlags, rc: ReasonCode, _props: Properties | None
@@ -103,8 +103,8 @@ class MqttClient:
103
103
  log.error("Invalid MQTT credentials", result_code=rc)
104
104
 
105
105
  self.log.info("Connected to broker", result_code=rc)
106
- for topic in self.providers_by_topic:
107
- 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)
108
108
  self.client.subscribe(topic)
109
109
 
110
110
  def on_disconnect(
@@ -272,7 +272,8 @@ class MqttClient:
272
272
  if msg.topic in self.providers_by_topic:
273
273
  self.handle_message(msg)
274
274
  else:
275
- 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)
276
277
 
277
278
  def handle_message(self, msg: mqtt.MQTTMessage | LocalMessage) -> None:
278
279
  def update_start(discovery: Discovery) -> None:
@@ -314,12 +315,14 @@ class MqttClient:
314
315
  self.publish(
315
316
  self.config_topic(discovery),
316
317
  hass_format_config(
317
- discovery,
318
- object_id,
319
- self.node_cfg.name,
320
- self.state_topic(discovery),
321
- command_topic,
322
- discovery.session,
318
+ discovery=discovery,
319
+ object_id=object_id,
320
+ node_name=self.node_cfg.name,
321
+ area=self.hass_cfg.area,
322
+ state_topic=self.state_topic(discovery),
323
+ command_topic=command_topic,
324
+ device_creation=self.hass_cfg.device_creation,
325
+ session=discovery.session,
323
326
  ),
324
327
  )
325
328
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: updates2mqtt
3
- Version: 1.4.0
3
+ Version: 1.4.2
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
@@ -35,18 +35,27 @@ Requires-Dist: structlog>=25.4.0
35
35
  Requires-Dist: usingversion>=0.1.2
36
36
  Description-Content-Type: text/markdown
37
37
 
38
- [![Rhizomatics Open Source](https://avatars.githubusercontent.com/u/162821163?s=96&v=4)](https://github.com/rhizomatics)
38
+ ![updates2mqtt](/images/updates2mqtt-dark-256x256.png){ align=left }
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/refs/heads/badges/badges/coverage.svg)](https://updates2mqtt.rhizomatics.org.uk/developer/coverage/)
47
+ ![Tests](https://raw.githubusercontent.com/rhizomatics/updates2mqtt/refs/heads/badges/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)
47
51
  [![CodeQL](https://github.com/rhizomatics/updates2mqtt/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/rhizomatics/updates2mqtt/actions/workflows/github-code-scanning/codeql)
48
52
  [![Dependabot Updates](https://github.com/rhizomatics/updates2mqtt/actions/workflows/dependabot/dependabot-updates/badge.svg)](https://github.com/rhizomatics/updates2mqtt/actions/workflows/dependabot/dependabot-updates)
49
53
 
54
+
55
+ <br/>
56
+ <br/>
57
+
58
+
50
59
  ## Summary
51
60
 
52
61
  Let Home Assistant tell you about new updates to Docker images for your containers.
@@ -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=G7_5L7Ypc7dl35nnd5dP2AobSA08ZuyEM_ETCTPg_co,5144
5
+ updates2mqtt/hass_formatter.py,sha256=GrVtxqpaXNFaQWXjmOFhOxYwXVjeNjzUwAjFdJdsrwI,3337
6
+ updates2mqtt/model.py,sha256=O6GQFhfQvwQDxlZScFHrpQgFNkUrUAeaGGP6AqNua78,3827
7
+ updates2mqtt/mqtt.py,sha256=I8g-SekY-AIVRcSCZyb7fTGji5a8zT1PJ7d34l7LGyY,14832
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.2.dist-info/METADATA,sha256=1wqWYIbcNbC-SQxk2FiI_SPmw9F-RFwdoOE_vviSXzw,9290
13
+ updates2mqtt-1.4.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
14
+ updates2mqtt-1.4.2.dist-info/entry_points.txt,sha256=Hc6NZ2dBevYSUKTJU6NOs8Mw7Vt0S-9lq5FuKb76NCc,54
15
+ updates2mqtt-1.4.2.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
16
+ updates2mqtt-1.4.2.dist-info/RECORD,,
@@ -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=SK6uhDyUb9C2JYVd0j6KBHzSAfaCFcOUbmmgsq6VSs0,5027
5
- updates2mqtt/hass_formatter.py,sha256=Ulfj8F0e_1QMmRuJzHsNM2WxHbz9sIkWOWjRq3kQZzs,2772
6
- updates2mqtt/model.py,sha256=5tWlj3appGGZjkuBeYR2lb-kXoy5mzCn4P_EJQjnwok,3676
7
- updates2mqtt/mqtt.py,sha256=LDy9x7Mmq_Em6lV-w9J3TjzBCuu8eehI8PK4E0i015A,14468
8
- updates2mqtt/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- updates2mqtt/integrations/__init__.py,sha256=KmNTUxvVWvqI7rl4I0xZg7XaCmcMS2O4OSv-ClsWM4Q,109
10
- updates2mqtt/integrations/docker.py,sha256=c1y3Xkv57_frxZOlq9LoYQRnfX-1XeedCjDcBLx1it0,18849
11
- updates2mqtt/integrations/git_utils.py,sha256=bPCmQiZpKpMcrGI7xAVmePXHFn8WwjcPNkf7xqDsGQA,2319
12
- updates2mqtt-1.4.0.dist-info/METADATA,sha256=0wYbNmGsu4nQF5C49M0pegMkpmMgW7rNR5n6FTbTwlA,8916
13
- updates2mqtt-1.4.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
14
- updates2mqtt-1.4.0.dist-info/entry_points.txt,sha256=Hc6NZ2dBevYSUKTJU6NOs8Mw7Vt0S-9lq5FuKb76NCc,54
15
- updates2mqtt-1.4.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
16
- updates2mqtt-1.4.0.dist-info/RECORD,,