updates2mqtt 1.5.1__py3-none-any.whl → 1.7.0__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
@@ -14,7 +14,7 @@ import structlog
14
14
  import updates2mqtt
15
15
  from updates2mqtt.model import Discovery, ReleaseProvider
16
16
 
17
- from .config import Config, PackageUpdateInfo, load_app_config, load_package_info
17
+ from .config import Config, PublishPolicy, UpdatePolicy, load_app_config
18
18
  from .integrations.docker import DockerProvider
19
19
  from .mqtt import MqttPublisher
20
20
 
@@ -39,14 +39,16 @@ 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}, edit config to fix missing or invalid values and restart")
42
+ log.error(f"Invalid configuration at {CONF_FILE}")
43
+ log.error("Edit config to fix missing or invalid values and restart")
44
+ log.error("Alternately supply correct MQTT_HOST,MQTT_USER,MQTT_PASSWORD environment variables")
43
45
  log.error("Exiting app")
44
46
  sys.exit(1)
45
47
  self.cfg: Config = app_config
48
+ self.self_bounce: Event = Event()
46
49
 
47
50
  structlog.configure(wrapper_class=structlog.make_filtering_bound_logger(getattr(logging, str(self.cfg.log.level))))
48
51
  log.debug("Logging initialized", level=self.cfg.log.level)
49
- self.common_pkg: dict[str, PackageUpdateInfo] = load_package_info(PKG_INFO_FILE)
50
52
 
51
53
  self.publisher = MqttPublisher(self.cfg.mqtt, self.cfg.node, self.cfg.homeassistant)
52
54
 
@@ -54,7 +56,7 @@ class App:
54
56
  self.scan_count: int = 0
55
57
  self.last_scan: str | None = None
56
58
  if self.cfg.docker.enabled:
57
- self.scanners.append(DockerProvider(self.cfg.docker, self.common_pkg, self.cfg.node))
59
+ self.scanners.append(DockerProvider(self.cfg.docker, self.cfg.node, self.self_bounce))
58
60
  self.stopped = Event()
59
61
  self.healthcheck_topic = self.cfg.node.healthcheck.topic_template.format(node_name=self.cfg.node.name)
60
62
 
@@ -101,6 +103,7 @@ class App:
101
103
  )
102
104
 
103
105
  for scanner in self.scanners:
106
+ scanner.initialize()
104
107
  self.publisher.subscribe_hass_command(scanner)
105
108
 
106
109
  while not self.stopped.is_set() and self.publisher.is_available():
@@ -119,11 +122,18 @@ class App:
119
122
  async def on_discovery(self, discovery: Discovery) -> None:
120
123
  dlog = log.bind(name=discovery.name)
121
124
  try:
122
- if self.cfg.homeassistant.discovery.enabled:
125
+ if discovery.publish_policy == PublishPolicy.HOMEASSISTANT and self.cfg.homeassistant.discovery.enabled:
126
+ # Switch off MQTT discovery if not Home Assistant enabled
123
127
  self.publisher.publish_hass_config(discovery)
124
-
125
- self.publisher.publish_hass_state(discovery)
126
- if discovery.update_policy == "Auto":
128
+ if discovery.publish_policy in (PublishPolicy.HOMEASSISTANT):
129
+ self.publisher.publish_hass_state(discovery)
130
+ if discovery.publish_policy in (PublishPolicy.HOMEASSISTANT, PublishPolicy.MQTT):
131
+ self.publisher.publish_discovery(discovery)
132
+ if (
133
+ discovery.update_policy == UpdatePolicy.AUTO
134
+ and discovery.can_update
135
+ and discovery.latest_version != discovery.current_version
136
+ ):
127
137
  # TODO: review auto update, trigger by version, use update interval as throttle
128
138
  elapsed: float = (
129
139
  time.time() - discovery.update_last_attempt if discovery.update_last_attempt is not None else -1
@@ -155,7 +165,11 @@ class App:
155
165
  log.debug("Cancellation task completed")
156
166
 
157
167
  def shutdown(self, *args, exit_code: int = 143) -> None: # noqa: ANN002, ARG002
158
- log.info("Shutting down, exit_code: %s", exit_code)
168
+ if self.self_bounce.is_set():
169
+ exit_code = 1
170
+ log.info("Self bouncing, overriding exit_code: %s", exit_code)
171
+ else:
172
+ log.info("Shutting down, exit_code: %s", exit_code)
159
173
  self.stopped.set()
160
174
  for scanner in self.scanners:
161
175
  scanner.stop()
@@ -206,8 +220,6 @@ def run() -> None:
206
220
  import asyncio
207
221
  import signal
208
222
 
209
- from .app import App
210
-
211
223
  # pyright: ignore[reportAttributeAccessIssue]
212
224
  log.debug(f"Starting updates2mqtt v{updates2mqtt.version}") # pyright: ignore[reportAttributeAccessIssue]
213
225
  app = App()
updates2mqtt/config.py CHANGED
@@ -9,6 +9,20 @@ from omegaconf import MISSING, DictConfig, MissingMandatoryValue, OmegaConf, Val
9
9
 
10
10
  log = structlog.get_logger()
11
11
 
12
+ PKG_INFO_FILE = Path("./common_packages.yaml")
13
+ NO_KNOWN_IMAGE = "UNKNOWN"
14
+
15
+
16
+ class UpdatePolicy(StrEnum):
17
+ AUTO = "Auto"
18
+ PASSIVE = "Passive"
19
+
20
+
21
+ class PublishPolicy(StrEnum):
22
+ HOMEASSISTANT = "HomeAssistant"
23
+ MQTT = "MQTT"
24
+ SILENT = "Silent"
25
+
12
26
 
13
27
  class LogLevel(StrEnum):
14
28
  DEBUG = "DEBUG"
@@ -18,6 +32,13 @@ class LogLevel(StrEnum):
18
32
  CRITICAL = "CRITICAL"
19
33
 
20
34
 
35
+ class VersionType:
36
+ SHORT_SHA = "short_sha"
37
+ FULL_SHA = "full_sha"
38
+ VERSION_REVISION = "version_revision"
39
+ VERSION = "version"
40
+
41
+
21
42
  @dataclass
22
43
  class MqttConfig:
23
44
  host: str = "${oc.env:MQTT_HOST,localhost}"
@@ -34,6 +55,12 @@ class MetadataSourceConfig:
34
55
  cache_ttl: int = 60 * 60 * 24 * 7 # 1 week
35
56
 
36
57
 
58
+ @dataclass
59
+ class Selector:
60
+ include: list[str] | None = None
61
+ exclude: list[str] | None = None
62
+
63
+
37
64
  @dataclass
38
65
  class DockerConfig:
39
66
  enabled: bool = True
@@ -47,6 +74,8 @@ class DockerConfig:
47
74
  discover_metadata: dict[str, MetadataSourceConfig] = field(
48
75
  default_factory=lambda: {"linuxserver.io": MetadataSourceConfig(enabled=True)}
49
76
  )
77
+ default_api_backoff: int = 60 * 15
78
+ image_ref_select: Selector = field(default_factory=lambda: Selector())
50
79
 
51
80
 
52
81
  @dataclass
@@ -61,6 +90,7 @@ class HomeAssistantConfig:
61
90
  state_topic_suffix: str = "state"
62
91
  device_creation: bool = True
63
92
  force_command_topic: bool = False
93
+ extra_attributes: bool = True
64
94
  area: str | None = None
65
95
 
66
96
 
@@ -114,24 +144,6 @@ class IncompleteConfigException(BaseException):
114
144
  pass
115
145
 
116
146
 
117
- def load_package_info(pkginfo_file_path: Path) -> dict[str, PackageUpdateInfo]:
118
- if pkginfo_file_path.exists():
119
- log.debug("Loading common package update info", path=pkginfo_file_path)
120
- cfg = OmegaConf.load(pkginfo_file_path)
121
- else:
122
- log.warn("No common package update info found", path=pkginfo_file_path)
123
- cfg = OmegaConf.structured(UpdateInfoConfig)
124
- try:
125
- # omegaconf broken-ness on optional fields and converting to backclasses
126
- pkg_conf: dict[str, PackageUpdateInfo] = {
127
- pkg: PackageUpdateInfo(**pkg_cfg) for pkg, pkg_cfg in cfg.common_packages.items()
128
- }
129
- return pkg_conf
130
- except (MissingMandatoryValue, ValidationError) as e:
131
- log.error("Configuration error %s", e, path=pkginfo_file_path.as_posix())
132
- raise
133
-
134
-
135
147
  def is_autogen_config() -> bool:
136
148
  env_var: str | None = os.environ.get("U2M_AUTOGEN_CONFIG")
137
149
  return not (env_var and env_var.lower() in ("no", "0", "false"))
@@ -23,28 +23,26 @@ def hass_format_config(
23
23
  object_id: str,
24
24
  state_topic: str,
25
25
  command_topic: str | None,
26
+ attrs_topic: str | None,
26
27
  force_command_topic: bool | None,
27
28
  device_creation: bool = True,
28
29
  area: str | None = None,
29
- session: str | None = None,
30
30
  ) -> dict[str, Any]:
31
31
  config: dict[str, Any] = {
32
32
  "name": discovery.title,
33
33
  "device_class": None, # not firmware, so defaults to null
34
34
  "unique_id": object_id,
35
35
  "state_topic": state_topic,
36
- "source_session": session,
37
36
  "supported_features": discovery.features,
38
- "can_update": discovery.can_update,
39
- "can_build": discovery.can_build,
40
- "can_restart": discovery.can_restart,
41
- "update_policy": discovery.update_policy,
37
+ "default_entity_id": f"update.{discovery.node}_{discovery.provider.source_type}_{discovery.name}",
42
38
  "origin": {
43
39
  "name": f"{discovery.node} updates2mqtt",
44
40
  "sw_version": updates2mqtt.version, # pyright: ignore[reportAttributeAccessIssue]
45
41
  "support_url": "https://github.com/rhizomatics/updates2mqtt/issues",
46
42
  },
47
43
  }
44
+ if attrs_topic:
45
+ config["json_attributes_topic"] = attrs_topic
48
46
  if discovery.entity_picture_url:
49
47
  config["entity_picture"] = discovery.entity_picture_url
50
48
  if discovery.device_icon:
@@ -62,14 +60,12 @@ def hass_format_config(
62
60
  config["command_topic"] = command_topic
63
61
  if discovery.can_update:
64
62
  config["payload_install"] = f"{discovery.source_type}|{discovery.name}|install"
65
- if discovery.custom.get("git_repo_path"):
66
- config["git_repo_path"] = discovery.custom["git_repo_path"]
67
- config.update(discovery.provider.hass_config_format(discovery))
63
+
68
64
  return config
69
65
 
70
66
 
71
67
  def hass_format_state(discovery: Discovery, session: str, in_progress: bool = False) -> dict[str, Any]: # noqa: ARG001
72
- state = {
68
+ state: dict[str, str | dict | list | bool | None] = {
73
69
  "installed_version": discovery.current_version,
74
70
  "latest_version": discovery.latest_version,
75
71
  "title": discovery.title,
@@ -79,11 +75,5 @@ def hass_format_state(discovery: Discovery, session: str, in_progress: bool = Fa
79
75
  state["release_summary"] = discovery.release_summary
80
76
  if discovery.release_url:
81
77
  state["release_url"] = discovery.release_url
82
- custom_state = discovery.provider.hass_state_format(discovery)
83
- if custom_state:
84
- state.update(custom_state)
85
- invalid_keys = [k for k in state if k not in HASS_UPDATE_SCHEMA]
86
- if invalid_keys:
87
- log.warning(f"Invalid keys in state: {invalid_keys}")
88
- state = {k: v for k, v in state.items() if k in HASS_UPDATE_SCHEMA}
78
+
89
79
  return state