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 +23 -11
- updates2mqtt/config.py +30 -18
- updates2mqtt/hass_formatter.py +7 -17
- updates2mqtt/integrations/docker.py +303 -151
- updates2mqtt/integrations/docker_enrich.py +344 -0
- updates2mqtt/integrations/git_utils.py +71 -14
- updates2mqtt/model.py +150 -16
- updates2mqtt/mqtt.py +28 -7
- {updates2mqtt-1.5.1.dist-info → updates2mqtt-1.7.0.dist-info}/METADATA +68 -33
- updates2mqtt-1.7.0.dist-info/RECORD +16 -0
- {updates2mqtt-1.5.1.dist-info → updates2mqtt-1.7.0.dist-info}/WHEEL +1 -1
- {updates2mqtt-1.5.1.dist-info → updates2mqtt-1.7.0.dist-info}/entry_points.txt +1 -0
- updates2mqtt-1.5.1.dist-info/RECORD +0 -16
- updates2mqtt-1.5.1.dist-info/licenses/LICENSE +0 -201
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,
|
|
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}
|
|
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.
|
|
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
|
-
|
|
126
|
-
if discovery.
|
|
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
|
-
|
|
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"))
|
updates2mqtt/hass_formatter.py
CHANGED
|
@@ -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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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
|