updates2mqtt 1.4.0__tar.gz → 1.4.1__tar.gz
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-1.4.0 → updates2mqtt-1.4.1}/.github/workflows/docker-publish.yml +2 -2
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/.github/workflows/pypi-publish.yml +4 -1
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/.github/workflows/python-package.yml +23 -2
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/.gitignore +1 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/CHANGELOG.md +5 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/PKG-INFO +5 -1
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/README.md +4 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/conftest.py +68 -5
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/docs/installation.md +13 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/docs/troubleshooting.md +72 -2
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/pyproject.toml +5 -2
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/src/updates2mqtt/app.py +15 -10
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/src/updates2mqtt/integrations/docker.py +10 -4
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/src/updates2mqtt/model.py +4 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/src/updates2mqtt/mqtt.py +6 -5
- updates2mqtt-1.4.1/tests/test_app.py +130 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/tests/test_mqtt.py +6 -6
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/uv.lock +108 -1
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/.dockerignore +0 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/.github/dependabot.yml +0 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/.hintrc +0 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/.pre-commit-config.yaml +0 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/.python-version +0 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/.safety-project.ini +0 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/Dockerfile +0 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/LICENSE +0 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/SECURITY.md +0 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/common_packages.yaml +0 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/docs/configuration.md +0 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/docs/examples/config_maximal.md +0 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/docs/examples/config_minimal.md +0 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/docs/examples/docker_compose.md +0 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/docs/home_assistant.md +0 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/docs/images/ha_entities.png +0 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/docs/images/ha_mqtt_discovery.png +0 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/docs/images/ha_update_detail.png +0 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/docs/images/ha_update_dialog.png +0 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/docs/images/ha_update_page.png +0 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/docs/images/logo-blank-256x256.png +0 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/docs/index.md +0 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/docs/local_builds.md +0 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/examples/config.yaml.maximal +0 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/examples/config.yaml.minimal +0 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/examples/docker-compose.yaml +0 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/mkdocs.yml +0 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/no_config.yaml +0 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/refresh-deps.sh +0 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/scripts/healthcheck.sh +0 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/src/updates2mqtt/__init__.py +0 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/src/updates2mqtt/__main__.py +0 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/src/updates2mqtt/config.py +0 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/src/updates2mqtt/hass_formatter.py +0 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/src/updates2mqtt/integrations/__init__.py +0 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/src/updates2mqtt/integrations/git_utils.py +0 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/src/updates2mqtt/py.typed +0 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/tests/__init__.py +0 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/tests/test_config.py +0 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/tests/test_docker.py +0 -0
- {updates2mqtt-1.4.0 → updates2mqtt-1.4.1}/tests/test_git_utils.py +0 -0
|
@@ -25,7 +25,7 @@ jobs:
|
|
|
25
25
|
#
|
|
26
26
|
steps:
|
|
27
27
|
- name: Checkout repository
|
|
28
|
-
uses: actions/checkout@
|
|
28
|
+
uses: actions/checkout@v6
|
|
29
29
|
# Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here.
|
|
30
30
|
- name: Log in to the Container registry
|
|
31
31
|
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef
|
|
@@ -42,7 +42,7 @@ jobs:
|
|
|
42
42
|
uses: docker/setup-buildx-action@v3
|
|
43
43
|
- name: Extract metadata (tags, labels) for Docker
|
|
44
44
|
id: meta
|
|
45
|
-
uses: docker/metadata-action@
|
|
45
|
+
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051
|
|
46
46
|
with:
|
|
47
47
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
|
48
48
|
tags: |
|
|
@@ -2,13 +2,16 @@ name: Publish Python 🐍 distribution 📦 to PyPI and TestPyPI
|
|
|
2
2
|
|
|
3
3
|
on: push
|
|
4
4
|
|
|
5
|
+
permissions:
|
|
6
|
+
contents: read
|
|
7
|
+
|
|
5
8
|
jobs:
|
|
6
9
|
build:
|
|
7
10
|
name: Build distribution 📦
|
|
8
11
|
runs-on: ubuntu-latest
|
|
9
12
|
|
|
10
13
|
steps:
|
|
11
|
-
- uses: actions/checkout@
|
|
14
|
+
- uses: actions/checkout@v6
|
|
12
15
|
with:
|
|
13
16
|
persist-credentials: false
|
|
14
17
|
- name: uv-setup
|
|
@@ -15,7 +15,7 @@ jobs:
|
|
|
15
15
|
permissions:
|
|
16
16
|
contents: write
|
|
17
17
|
steps:
|
|
18
|
-
- uses: actions/checkout@
|
|
18
|
+
- uses: actions/checkout@v6
|
|
19
19
|
- name: uv-setup
|
|
20
20
|
# Install a specific uv version using the installer
|
|
21
21
|
run: curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
@@ -27,11 +27,32 @@ jobs:
|
|
|
27
27
|
run: uv sync --all-extras --dev
|
|
28
28
|
|
|
29
29
|
- name: Ruff
|
|
30
|
-
uses:
|
|
30
|
+
uses: astral-sh/ruff-action@v3
|
|
31
31
|
- name: Test with pytest
|
|
32
32
|
run: uv run pytest tests
|
|
33
33
|
- name: Build a binary wheel and a source tarball
|
|
34
34
|
run: uv build
|
|
35
|
+
- name: Generate badges
|
|
36
|
+
env:
|
|
37
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
38
|
+
run: |
|
|
39
|
+
uv run genbadge coverage -i cov.xml -o badges/coverage.svg
|
|
40
|
+
uv run genbadge tests -i junit/test-results.xml -o badges/tests.svg
|
|
41
|
+
|
|
42
|
+
git config --global user.name "github-actions[bot]"
|
|
43
|
+
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
|
44
|
+
TEMP_DIR=$(mktemp -d)
|
|
45
|
+
cd $TEMP_DIR
|
|
46
|
+
git clone --single-branch --branch badges https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git . 2>/dev/null
|
|
47
|
+
cp -r ${{ github.workspace }}/badges .
|
|
48
|
+
git add badges
|
|
49
|
+
if git commit -m "Update badges [skip ci]"; then
|
|
50
|
+
git push origin badges --force
|
|
51
|
+
echo "Badges updated successfully"
|
|
52
|
+
else
|
|
53
|
+
echo "No changes to badges"
|
|
54
|
+
fi
|
|
55
|
+
|
|
35
56
|
- name: Prep for mkdocs
|
|
36
57
|
run: uv pip compile pyproject.toml -o requirements_docs.txt --group mkdocs
|
|
37
58
|
- name: Deploy docs
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 1.4.1
|
|
4
|
+
- More logging for Docker discovery on why Home Assistant doesn't show an update button
|
|
5
|
+
- More test cases
|
|
6
|
+
- `MqttClient` is now `MqttPublisher` to avoid confusion with actual MQTT client
|
|
7
|
+
- Task cleanup now only interrupts explicit list of tasks - healthcheck and discovery tasks
|
|
3
8
|
## 1.4.0
|
|
4
9
|
- MQTT protocol can now be set, to one of `3.1`,`3.11` or `5`
|
|
5
10
|
- Debug messages now provided for `on_subscribe` and `on_unsubscribe` callbacks
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: updates2mqtt
|
|
3
|
-
Version: 1.4.
|
|
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
|
+
[](https://github.com/rhizomatics)
|
|
43
|
+
|
|
42
44
|
[](https://pypi.org/project/updates2mqtt/)
|
|
43
45
|
[](https://github.com/rhizomatics/supernotify)
|
|
46
|
+

|
|
47
|
+

|
|
44
48
|
[](https://results.pre-commit.ci/latest/github/rhizomatics/updates2mqtt/main)
|
|
45
49
|
[](https://github.com/rhizomatics/updates2mqtt/actions/workflows/pypi-publish.yml)
|
|
46
50
|
[](https://github.com/rhizomatics/updates2mqtt/actions/workflows/python-package.yml)
|
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
# updates2mqtt
|
|
4
4
|
|
|
5
|
+
[](https://github.com/rhizomatics)
|
|
6
|
+
|
|
5
7
|
[](https://pypi.org/project/updates2mqtt/)
|
|
6
8
|
[](https://github.com/rhizomatics/supernotify)
|
|
9
|
+

|
|
10
|
+

|
|
7
11
|
[](https://results.pre-commit.ci/latest/github/rhizomatics/updates2mqtt/main)
|
|
8
12
|
[](https://github.com/rhizomatics/updates2mqtt/actions/workflows/pypi-publish.yml)
|
|
9
13
|
[](https://github.com/rhizomatics/updates2mqtt/actions/workflows/python-package.yml)
|
|
@@ -1,26 +1,89 @@
|
|
|
1
|
-
|
|
1
|
+
# python
|
|
2
|
+
from collections.abc import AsyncGenerator, Callable
|
|
3
|
+
from typing import Any
|
|
4
|
+
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
|
2
5
|
|
|
3
6
|
import paho.mqtt.client
|
|
4
7
|
import pytest
|
|
5
8
|
from docker import DockerClient
|
|
6
9
|
from docker.models.containers import Container, ContainerCollection
|
|
7
10
|
from docker.models.images import Image, RegistryData
|
|
11
|
+
from omegaconf import DictConfig, OmegaConf
|
|
8
12
|
|
|
13
|
+
import updates2mqtt.app
|
|
14
|
+
from updates2mqtt.app import (
|
|
15
|
+
App, # relative import as required
|
|
16
|
+
MqttPublisher,
|
|
17
|
+
)
|
|
18
|
+
from updates2mqtt.config import Config
|
|
9
19
|
from updates2mqtt.model import Discovery, ReleaseProvider
|
|
10
20
|
|
|
11
21
|
|
|
22
|
+
@pytest.fixture
|
|
23
|
+
def app_with_mocked_external_dependencies(
|
|
24
|
+
monkeypatch, # noqa: ANN001
|
|
25
|
+
mock_provider_class: type,
|
|
26
|
+
mock_publisher_class: type,
|
|
27
|
+
) -> App:
|
|
28
|
+
cfg: DictConfig = OmegaConf.structured(Config)
|
|
29
|
+
monkeypatch.setattr(updates2mqtt.app, "load_app_config", lambda *_args, **__kwargs: cfg)
|
|
30
|
+
monkeypatch.setattr(updates2mqtt.app, "DockerProvider", mock_provider_class)
|
|
31
|
+
monkeypatch.setattr(updates2mqtt.app, "MqttPublisher", mock_publisher_class)
|
|
32
|
+
app: App = App()
|
|
33
|
+
return app
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@pytest.fixture
|
|
37
|
+
def mock_discoveries(mock_provider: ReleaseProvider) -> list[Discovery]:
|
|
38
|
+
return [Discovery(mock_provider, "thing-1", "test001")]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@pytest.fixture
|
|
42
|
+
def mock_discovery_generator(mock_discoveries: list[Discovery]) -> Callable[..., AsyncGenerator[Discovery, Any]]:
|
|
43
|
+
async def g(*args: Any) -> AsyncGenerator[Discovery]: # noqa: ARG001
|
|
44
|
+
for d in mock_discoveries:
|
|
45
|
+
yield d
|
|
46
|
+
|
|
47
|
+
return g
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@pytest.fixture
|
|
51
|
+
def mock_provider_class(mock_provider: ReleaseProvider) -> type:
|
|
52
|
+
class MockReleaseProvider(ReleaseProvider):
|
|
53
|
+
def __new__(cls, *args: Any, **kwargs: Any) -> ReleaseProvider: # type: ignore[misc] # noqa: ARG004
|
|
54
|
+
return mock_provider
|
|
55
|
+
|
|
56
|
+
return MockReleaseProvider
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@pytest.fixture
|
|
60
|
+
def mock_publisher_class(mock_publisher: MqttPublisher) -> type:
|
|
61
|
+
class MockPublisher(MqttPublisher):
|
|
62
|
+
def __new__(cls, *args: Any, **kwargs: Any) -> MqttPublisher: # type: ignore[misc] # noqa: ARG004
|
|
63
|
+
return mock_publisher
|
|
64
|
+
|
|
65
|
+
return MockPublisher
|
|
66
|
+
|
|
67
|
+
|
|
12
68
|
@pytest.fixture
|
|
13
69
|
def mock_provider() -> ReleaseProvider:
|
|
14
|
-
provider =
|
|
70
|
+
provider: ReleaseProvider = AsyncMock(spec=ReleaseProvider)
|
|
15
71
|
provider.source_type = "unit_test"
|
|
16
|
-
provider.command.return_value = True
|
|
17
|
-
provider.resolve.return_value = Discovery(
|
|
72
|
+
provider.command.return_value = True # type: ignore[attr-defined]
|
|
73
|
+
provider.resolve.return_value = Discovery( # type: ignore[attr-defined]
|
|
18
74
|
provider, "fooey", session="test-mqtt-123", current_version="v2", latest_version="v2"
|
|
19
75
|
)
|
|
20
|
-
provider.hass_state_format.return_value = {"fixture": "test_exec"}
|
|
76
|
+
provider.hass_state_format.return_value = {"fixture": "test_exec"} # type: ignore[attr-defined]
|
|
21
77
|
return provider
|
|
22
78
|
|
|
23
79
|
|
|
80
|
+
@pytest.fixture
|
|
81
|
+
def mock_publisher(mock_mqtt_client: paho.mqtt.client.Client) -> MqttPublisher:
|
|
82
|
+
publisher: MqttPublisher = AsyncMock(MqttPublisher)
|
|
83
|
+
publisher.client = mock_mqtt_client
|
|
84
|
+
return publisher
|
|
85
|
+
|
|
86
|
+
|
|
24
87
|
@pytest.fixture
|
|
25
88
|
def mock_mqtt_client() -> paho.mqtt.client.Client:
|
|
26
89
|
return MagicMock(spec=paho.mqtt.client.Client, name="MQTT Client Fixture")
|
|
@@ -12,6 +12,19 @@ See `examples` directory for a working `docker-compose.yaml`.
|
|
|
12
12
|
|
|
13
13
|
If you want to update and restart containers, then the file system paths to the location of the directory where the docker compose file lives must be available in the updates2mqtt container.
|
|
14
14
|
|
|
15
|
+
```yaml
|
|
16
|
+
volumes:
|
|
17
|
+
# Must have config directory mapped
|
|
18
|
+
- ./conf:/app/conf
|
|
19
|
+
# Must have the Docker daemon socket mapped
|
|
20
|
+
- /var/run/docker.sock:/var/run/docker.sock
|
|
21
|
+
# This list of paths is only needed when containers are to be updated
|
|
22
|
+
# The paths here are completely dependent on where your docker-compose files live
|
|
23
|
+
- /home/containers:/home/containers
|
|
24
|
+
- /dev.containers:/dev.containers
|
|
25
|
+
- /containers:/containers
|
|
26
|
+
```
|
|
27
|
+
|
|
15
28
|
The example `docker-compose.yaml` mounts `/home/containers` for this purpose, so if your containers are in
|
|
16
29
|
`/home/containers/app1`, `/home/containers/app2` etc, then updates2mqtt will be able to find them. Map as many root paths as needed.
|
|
17
30
|
|
|
@@ -1,14 +1,52 @@
|
|
|
1
1
|
# Troubleshooting
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## General
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Things that need to work:
|
|
6
|
+
|
|
7
|
+
- Docker API access to list and inspect containers
|
|
8
|
+
- MQTT Publication of results
|
|
9
|
+
- Home Assistant discovering the Update entities on MQTT
|
|
10
|
+
- Home Assistant generating update notices in UI when there's a new version
|
|
11
|
+
- Command Topic message sent by Home Assistant when Update button clicked
|
|
12
|
+
- Docker Compose available from shell to make updates and restart
|
|
13
|
+
- Git command available in shell to check for local repo updates and pull
|
|
14
|
+
|
|
15
|
+
## Updates2MQTT Logs
|
|
16
|
+
|
|
17
|
+
If running under docker, and following the container naming guidance, then see the
|
|
18
|
+
logs using:
|
|
19
|
+
|
|
20
|
+
`docker logs updates2mqtt`
|
|
21
|
+
|
|
22
|
+
or change to the directory where the `docker-compose.yaml` is installed and do `docker compose logs`
|
|
23
|
+
|
|
24
|
+
### Changing Log Level
|
|
25
|
+
|
|
26
|
+
Update the `config.yaml` and change the log level to DEBUG, which will show much
|
|
27
|
+
more diagnostic information.
|
|
6
28
|
|
|
7
29
|
```yaml
|
|
8
30
|
log:
|
|
9
31
|
level: DEBUG
|
|
10
32
|
```
|
|
11
33
|
|
|
34
|
+
When you have everything working, its best to change the log level back, so
|
|
35
|
+
your container isn't generating big logs.
|
|
36
|
+
|
|
37
|
+
### Going Inside Container
|
|
38
|
+
|
|
39
|
+
From the `docker-compose.yaml` directory, execute
|
|
40
|
+
|
|
41
|
+
`docker compose exec -it updates2mqtt bash`
|
|
42
|
+
|
|
43
|
+
(if you have an old Docker install, you may need to use `docker-compose` instead of `docker compose`)
|
|
44
|
+
|
|
45
|
+
This will give you shell access inside the container, which is a good way of checking
|
|
46
|
+
for path issues, permissio issues etc. For example, if you have compose directories in the
|
|
47
|
+
`/containers` directory, you could `cd /containers` and validate that Updates2MQTT can see the
|
|
48
|
+
other compose directories, `ls` the contents, and run `docker compose` or `git` actions there.
|
|
49
|
+
|
|
12
50
|
## MQTT
|
|
13
51
|
|
|
14
52
|
If the host, port, user and password for MQTT are incorrect then usually this
|
|
@@ -58,6 +96,38 @@ Oddly, the Paho MQTT client used by Updates2MQTT is known to [report success eve
|
|
|
58
96
|
|
|
59
97
|
There's also an alternative to MQTT Discovery in HA, using plain yaml, the [MQTT Update Integration](https://www.home-assistant.io/integrations/update.mqtt/#configuration). The [BBQKees Boiler Gateway](https://bbqkees-electronics.nl/wiki/home-automations/home-assistant-configuration.html) has some detailed steps and examples for MQTT Discovery too.
|
|
60
98
|
|
|
99
|
+
#### No Update Button
|
|
100
|
+
|
|
101
|
+
If there's an update showing, but no *Update* button present, then there's a few reasons, which
|
|
102
|
+
can be checked directly from the config and log:
|
|
103
|
+
|
|
104
|
+
- Config has the `allow_pull`,`allow_restart` and `allow_build` all overridden to `False`
|
|
105
|
+
- A new version reference can't be found
|
|
106
|
+
- The compose working directory can't be found
|
|
107
|
+
- This is sourced from the `com.docker.compose.project.working_dir` label, which can be seen in `docker inspect`
|
|
108
|
+
- This only stops restart, not pull, so if `allow_pull` is on, the Update button will still show
|
|
109
|
+
- The git repo path can't be found for a local build
|
|
110
|
+
|
|
111
|
+
The current state of this can be seen in MQTT, the config message will have two extra
|
|
112
|
+
values as below:
|
|
113
|
+
|
|
114
|
+
```yaml
|
|
115
|
+
"command_topic": "updates2mqtt/dockernuc/docker",
|
|
116
|
+
"payload_install": "docker|homarr|install"
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
#### Home Assistant Logs
|
|
120
|
+
|
|
121
|
+
Use the [System Log](https://www.home-assistant.io/integrations/system_log/) to check
|
|
122
|
+
for MQTT errors, or for positive confirmation that Update entities have been discovered. The
|
|
123
|
+
*raw* log will show more, and allow you to scroll back for hours.
|
|
124
|
+
|
|
125
|
+
The [Logger Integration](https://www.home-assistant.io/integrations/logger/#viewing-logs)
|
|
126
|
+
lets you tweak the levels of specific integrations. This is less useful for Updates2MQTT,
|
|
127
|
+
since all the work is happening outside of Home Assistant, however it can be useful
|
|
128
|
+
for general MQTT issues, and the examples in the Home Assistant documentation shows how
|
|
129
|
+
to tune the MQTT integration.
|
|
130
|
+
|
|
61
131
|
## Docker
|
|
62
132
|
|
|
63
133
|
More detailed information on the Docker API and compatibility with Docker engine versions can be found at Docker's own [Docker Engine API](https://docs.docker.com/reference/api/engine/) reference.
|
|
@@ -7,7 +7,7 @@ authors = [
|
|
|
7
7
|
]
|
|
8
8
|
|
|
9
9
|
requires-python = ">=3.13"
|
|
10
|
-
version = "1.4.
|
|
10
|
+
version = "1.4.1"
|
|
11
11
|
license="Apache-2.0"
|
|
12
12
|
keywords=["mqtt", "docker", "updates", "automation","home-assistant","homeassistant","selfhosting"]
|
|
13
13
|
|
|
@@ -56,6 +56,8 @@ dev = [
|
|
|
56
56
|
"pytest-mqtt>=0.6.0",
|
|
57
57
|
"pytest-subprocess>=1.5.3",
|
|
58
58
|
"coverage",
|
|
59
|
+
"icdiff",
|
|
60
|
+
"genbadge[all]"
|
|
59
61
|
]
|
|
60
62
|
mkdocs=[
|
|
61
63
|
"mkdocs-material",
|
|
@@ -154,10 +156,11 @@ norecursedirs = [
|
|
|
154
156
|
".git",
|
|
155
157
|
"templates",
|
|
156
158
|
]
|
|
159
|
+
|
|
157
160
|
pythonpath = [
|
|
158
161
|
"."
|
|
159
162
|
]
|
|
160
|
-
addopts = "--cov --cov-report=lcov:lcov.info --cov-report=term --import-mode=importlib"
|
|
163
|
+
addopts = "--junitxml=junit/test-results.xml --cov --cov-report=lcov:lcov.info --cov-report=xml --cov-report=html --cov-report=term --import-mode=importlib"
|
|
161
164
|
|
|
162
165
|
[tool.coverage.run]
|
|
163
166
|
branch = true
|
|
@@ -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
|
|
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 =
|
|
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): #
|
|
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
|
|
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.
|
|
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(
|
|
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.
|
|
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")
|
|
@@ -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")
|
|
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
|
|
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]:
|
|
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():
|
|
@@ -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
|
|
@@ -28,7 +28,7 @@ class LocalMessage:
|
|
|
28
28
|
payload: str | None = field(default=None)
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
class
|
|
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
|
-
|
|
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:
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# python
|
|
2
|
+
import asyncio
|
|
3
|
+
import signal
|
|
4
|
+
import time
|
|
5
|
+
import types
|
|
6
|
+
from collections.abc import AsyncGenerator, Coroutine
|
|
7
|
+
from typing import Any, NoReturn
|
|
8
|
+
from unittest.mock import ANY, call
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
from updates2mqtt.app import App, run # relative import as required
|
|
13
|
+
from updates2mqtt.model import Discovery
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def test_scan(
|
|
17
|
+
app_with_mocked_external_dependencies: App,
|
|
18
|
+
mock_discoveries: list[Discovery],
|
|
19
|
+
mock_discovery_generator: AsyncGenerator[Discovery],
|
|
20
|
+
monkeypatch, # noqa: ANN001
|
|
21
|
+
) -> None:
|
|
22
|
+
uut: App = app_with_mocked_external_dependencies
|
|
23
|
+
monkeypatch.setattr(uut.scanners[0], "scan", mock_discovery_generator)
|
|
24
|
+
await uut.scan()
|
|
25
|
+
uut.publisher.clean_topics.assert_has_calls( # type: ignore[attr-defined]
|
|
26
|
+
[call(uut.scanners[0], None, force=True), call(uut.scanners[0], ANY, force=False)]
|
|
27
|
+
)
|
|
28
|
+
uut.publisher.publish_hass_state.assert_has_calls([call(d) for d in mock_discoveries]) # type: ignore[attr-defined]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def test_main_loop(
|
|
32
|
+
app_with_mocked_external_dependencies: App,
|
|
33
|
+
mock_discovery_generator: AsyncGenerator[Discovery],
|
|
34
|
+
monkeypatch, # noqa: ANN001
|
|
35
|
+
) -> None:
|
|
36
|
+
uut: App = app_with_mocked_external_dependencies
|
|
37
|
+
monkeypatch.setattr(uut.scanners[0], "scan", mock_discovery_generator)
|
|
38
|
+
start_time = time.time()
|
|
39
|
+
monkeypatch.setattr(uut.publisher, "is_available", lambda: time.time() > start_time + 3)
|
|
40
|
+
|
|
41
|
+
with pytest.raises(SystemExit):
|
|
42
|
+
await uut.main_loop()
|
|
43
|
+
uut.publisher.assert_has_calls( # type: ignore[attr-defined]
|
|
44
|
+
[
|
|
45
|
+
call.start(),
|
|
46
|
+
call.subscribe_hass_command(uut.scanners[0]),
|
|
47
|
+
call.stop(),
|
|
48
|
+
]
|
|
49
|
+
) # pyright: ignore[reportAttributeAccessIssue]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class DummyApp:
|
|
53
|
+
"""Dummy App to replace updates2mqtt.app.App during tests.
|
|
54
|
+
|
|
55
|
+
Records the created instance on DummyApp.instance for assertions.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
instance = None
|
|
59
|
+
|
|
60
|
+
def __init__(self) -> None:
|
|
61
|
+
DummyApp.instance = self
|
|
62
|
+
self.run_called = False
|
|
63
|
+
self.shutdown_called = False
|
|
64
|
+
|
|
65
|
+
async def main_loop(self) -> None:
|
|
66
|
+
# an async method to mirror the real App.run signature
|
|
67
|
+
self.run_called = True
|
|
68
|
+
|
|
69
|
+
def shutdown(self, *args: Any, **kwargs: Any) -> None: # noqa: ARG002
|
|
70
|
+
self.shutdown_called = True
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_run_sets_signal_and_calls_asyncio_run(monkeypatch) -> None: # noqa: ANN001
|
|
74
|
+
calls: dict[str, Any] = {}
|
|
75
|
+
|
|
76
|
+
# Replace signal.signal so we capture its arguments
|
|
77
|
+
def fake_signal(sig: int, handler: types.MethodType) -> None:
|
|
78
|
+
calls["sig"] = sig
|
|
79
|
+
calls["handler"] = handler
|
|
80
|
+
|
|
81
|
+
monkeypatch.setattr(signal, "signal", fake_signal)
|
|
82
|
+
|
|
83
|
+
# Patch the App class in the updates2mqtt.app module to DummyApp
|
|
84
|
+
import updates2mqtt.app as app_module
|
|
85
|
+
|
|
86
|
+
monkeypatch.setattr(app_module, "App", DummyApp)
|
|
87
|
+
|
|
88
|
+
# Patch asyncio.run to record that it was called with a coroutine and debug flag
|
|
89
|
+
def fake_asyncio_run(coro: Coroutine, debug: bool = False) -> None:
|
|
90
|
+
calls["coro"] = coro
|
|
91
|
+
calls["debug"] = debug
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
monkeypatch.setattr(asyncio, "run", fake_asyncio_run)
|
|
95
|
+
|
|
96
|
+
# Execute the run function under test
|
|
97
|
+
run()
|
|
98
|
+
|
|
99
|
+
# Assertions:
|
|
100
|
+
# - signal.signal was called with SIGTERM and the DummyApp.instance.shutdown bound method
|
|
101
|
+
assert calls.get("sig") == signal.SIGTERM
|
|
102
|
+
assert DummyApp.instance is not None
|
|
103
|
+
assert calls.get("handler") == DummyApp.instance.shutdown
|
|
104
|
+
# debug flag forwarded as False in run()
|
|
105
|
+
assert calls.get("debug") is False
|
|
106
|
+
# - asyncio.run was called with a coroutine object (the result of DummyApp.instance.run())
|
|
107
|
+
coro = calls.get("coro")
|
|
108
|
+
assert isinstance(coro, types.CoroutineType)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_run_handles_asyncio_cancellederror(monkeypatch) -> None: # noqa: ANN001
|
|
112
|
+
# Ensure App is replaced so run() will create DummyApp without side effects
|
|
113
|
+
import updates2mqtt.app as app_module
|
|
114
|
+
|
|
115
|
+
monkeypatch.setattr(app_module, "App", DummyApp)
|
|
116
|
+
|
|
117
|
+
# Patch signal.signal to a noop to avoid altering test process handlers
|
|
118
|
+
monkeypatch.setattr(signal, "signal", lambda *args, **kwargs: None) # noqa: ARG005
|
|
119
|
+
|
|
120
|
+
# Patch asyncio.run to raise CancelledError to exercise the except branch
|
|
121
|
+
def raising_asyncio_run(_coro: Coroutine, debug: bool = False) -> NoReturn: # noqa: ARG001
|
|
122
|
+
raise asyncio.CancelledError()
|
|
123
|
+
|
|
124
|
+
monkeypatch.setattr(asyncio, "run", raising_asyncio_run)
|
|
125
|
+
|
|
126
|
+
# Call run(); should not raise CancelledError (handled inside run)
|
|
127
|
+
run()
|
|
128
|
+
|
|
129
|
+
# If we reached here, the CancelledError was handled; also ensure DummyApp was instantiated
|
|
130
|
+
assert DummyApp.instance is not None
|
|
@@ -10,7 +10,7 @@ from paho.mqtt.client import MQTTMessage
|
|
|
10
10
|
|
|
11
11
|
from updates2mqtt.config import HomeAssistantConfig, MqttConfig, NodeConfig
|
|
12
12
|
from updates2mqtt.model import Discovery, ReleaseProvider
|
|
13
|
-
from updates2mqtt.mqtt import
|
|
13
|
+
from updates2mqtt.mqtt import MqttPublisher
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
@pytest.mark.parametrize("protocol", ["3", "3.1", "5", "?"])
|
|
@@ -20,7 +20,7 @@ def test_publish(mock_mqtt_client: Mock, protocol: str) -> None:
|
|
|
20
20
|
node_config = NodeConfig()
|
|
21
21
|
|
|
22
22
|
with patch.object(paho.mqtt.client.Client, "__new__", lambda *_args, **_kwargs: mock_mqtt_client):
|
|
23
|
-
uut =
|
|
23
|
+
uut = MqttPublisher(config, node_config, hass_config)
|
|
24
24
|
uut.start()
|
|
25
25
|
|
|
26
26
|
uut.publish("test.topic.123", {"foo": "a8", "bar": False})
|
|
@@ -35,7 +35,7 @@ async def test_handler(mock_mqtt_client: Mock) -> None:
|
|
|
35
35
|
node_config = NodeConfig()
|
|
36
36
|
node_config.name = "testing"
|
|
37
37
|
with patch("updates2mqtt.mqtt.mqtt.Client", new=mock_mqtt_client):
|
|
38
|
-
uut =
|
|
38
|
+
uut = MqttPublisher(config, node_config, hass_config)
|
|
39
39
|
uut.start(event_loop=asyncio.get_running_loop())
|
|
40
40
|
|
|
41
41
|
provider = Mock(spec=ReleaseProvider)
|
|
@@ -63,7 +63,7 @@ async def test_execute_command_remote(mock_mqtt_client: Mock, mock_provider: Rel
|
|
|
63
63
|
node_config = NodeConfig("TESTBED")
|
|
64
64
|
|
|
65
65
|
with patch.object(paho.mqtt.client.Client, "__new__", lambda *_args, **_kwargs: mock_mqtt_client):
|
|
66
|
-
uut =
|
|
66
|
+
uut = MqttPublisher(config, node_config, hass_config)
|
|
67
67
|
uut.start(event_loop=asyncio.get_running_loop())
|
|
68
68
|
|
|
69
69
|
uut.subscribe_hass_command(mock_provider)
|
|
@@ -95,7 +95,7 @@ async def test_execute_command_local(mock_mqtt_client: Mock, mock_provider: Rele
|
|
|
95
95
|
node_config = NodeConfig("TESTBED")
|
|
96
96
|
|
|
97
97
|
with patch.object(paho.mqtt.client.Client, "__new__", lambda *_args, **_kwargs: mock_mqtt_client):
|
|
98
|
-
uut =
|
|
98
|
+
uut = MqttPublisher(config, node_config, hass_config)
|
|
99
99
|
|
|
100
100
|
uut.start(event_loop=asyncio.get_running_loop())
|
|
101
101
|
|
|
@@ -127,7 +127,7 @@ async def test_stop(mock_mqtt_client: Mock, mock_provider: ReleaseProvider) -> N
|
|
|
127
127
|
node_config = NodeConfig()
|
|
128
128
|
|
|
129
129
|
with patch.object(paho.mqtt.client.Client, "__new__", lambda *_args, **_kwargs: mock_mqtt_client):
|
|
130
|
-
uut =
|
|
130
|
+
uut = MqttPublisher(config, node_config, hass_config)
|
|
131
131
|
|
|
132
132
|
uut.start(event_loop=asyncio.get_running_loop())
|
|
133
133
|
|
|
@@ -202,6 +202,15 @@ version = "0.9.5"
|
|
|
202
202
|
source = { registry = "https://pypi.org/simple" }
|
|
203
203
|
sdist = { url = "https://files.pythonhosted.org/packages/f1/2a/8c3ac3d8bc94e6de8d7ae270bb5bc437b210bb9d6d9e46630c98f4abd20c/csscompressor-0.9.5.tar.gz", hash = "sha256:afa22badbcf3120a4f392e4d22f9fff485c044a1feda4a950ecc5eba9dd31a05", size = 237808, upload-time = "2017-11-26T21:13:08.238Z" }
|
|
204
204
|
|
|
205
|
+
[[package]]
|
|
206
|
+
name = "defusedxml"
|
|
207
|
+
version = "0.7.1"
|
|
208
|
+
source = { registry = "https://pypi.org/simple" }
|
|
209
|
+
sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" }
|
|
210
|
+
wheels = [
|
|
211
|
+
{ url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" },
|
|
212
|
+
]
|
|
213
|
+
|
|
205
214
|
[[package]]
|
|
206
215
|
name = "distlib"
|
|
207
216
|
version = "0.4.0"
|
|
@@ -234,6 +243,55 @@ wheels = [
|
|
|
234
243
|
{ url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" },
|
|
235
244
|
]
|
|
236
245
|
|
|
246
|
+
[[package]]
|
|
247
|
+
name = "flake8"
|
|
248
|
+
version = "7.3.0"
|
|
249
|
+
source = { registry = "https://pypi.org/simple" }
|
|
250
|
+
dependencies = [
|
|
251
|
+
{ name = "mccabe" },
|
|
252
|
+
{ name = "pycodestyle" },
|
|
253
|
+
{ name = "pyflakes" },
|
|
254
|
+
]
|
|
255
|
+
sdist = { url = "https://files.pythonhosted.org/packages/9b/af/fbfe3c4b5a657d79e5c47a2827a362f9e1b763336a52f926126aa6dc7123/flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872", size = 48326, upload-time = "2025-06-20T19:31:35.838Z" }
|
|
256
|
+
wheels = [
|
|
257
|
+
{ url = "https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922, upload-time = "2025-06-20T19:31:34.425Z" },
|
|
258
|
+
]
|
|
259
|
+
|
|
260
|
+
[[package]]
|
|
261
|
+
name = "flake8-html"
|
|
262
|
+
version = "0.4.3"
|
|
263
|
+
source = { registry = "https://pypi.org/simple" }
|
|
264
|
+
dependencies = [
|
|
265
|
+
{ name = "flake8" },
|
|
266
|
+
{ name = "jinja2" },
|
|
267
|
+
{ name = "pygments" },
|
|
268
|
+
]
|
|
269
|
+
sdist = { url = "https://files.pythonhosted.org/packages/3e/44/faae17d4b4e00c8c041e9b90b0ea546f3aef8b799212e71a7bb835ff51e1/flake8-html-0.4.3.tar.gz", hash = "sha256:8b870299620cc4a06f73644a1b4d457799abeca1cc914c62ae71ec5bf65c79a5", size = 13670, upload-time = "2022-12-06T19:06:20.505Z" }
|
|
270
|
+
wheels = [
|
|
271
|
+
{ url = "https://files.pythonhosted.org/packages/c3/b0/bfd58118e2b9c1d0c5ac156c458c9a33cb599925b658df3ab038b55d30d7/flake8_html-0.4.3-py2.py3-none-any.whl", hash = "sha256:8f126748b1b0edd6cd39e87c6192df56e2f8655b0aa2bb00ffeac8cf27be4325", size = 13120, upload-time = "2022-12-06T19:06:18.917Z" },
|
|
272
|
+
]
|
|
273
|
+
|
|
274
|
+
[[package]]
|
|
275
|
+
name = "genbadge"
|
|
276
|
+
version = "1.1.3"
|
|
277
|
+
source = { registry = "https://pypi.org/simple" }
|
|
278
|
+
dependencies = [
|
|
279
|
+
{ name = "click" },
|
|
280
|
+
{ name = "pillow" },
|
|
281
|
+
{ name = "requests" },
|
|
282
|
+
{ name = "setuptools" },
|
|
283
|
+
]
|
|
284
|
+
sdist = { url = "https://files.pythonhosted.org/packages/88/08/686a720bd9f407a2b689c50a94e53b2d26f6ddc6f921ae45ec15c401ee67/genbadge-1.1.3.tar.gz", hash = "sha256:2292ea9cc20af4463dfde952c6b15544fdab9d6e50945f63a42cc400c521fa74", size = 138264, upload-time = "2025-11-24T14:55:01.342Z" }
|
|
285
|
+
wheels = [
|
|
286
|
+
{ url = "https://files.pythonhosted.org/packages/40/cc/e67b1fe7a9d76a316e9149855a953c37c463caf1e351b1a0abf7f2fb9e38/genbadge-1.1.3-py2.py3-none-any.whl", hash = "sha256:6e4316c171c6f0f84becae4eb116258340bdc054458632abc622d36b8040655e", size = 101262, upload-time = "2025-11-24T14:54:59.925Z" },
|
|
287
|
+
]
|
|
288
|
+
|
|
289
|
+
[package.optional-dependencies]
|
|
290
|
+
all = [
|
|
291
|
+
{ name = "defusedxml" },
|
|
292
|
+
{ name = "flake8-html" },
|
|
293
|
+
]
|
|
294
|
+
|
|
237
295
|
[[package]]
|
|
238
296
|
name = "ghp-import"
|
|
239
297
|
version = "2.1.0"
|
|
@@ -323,6 +381,15 @@ wheels = [
|
|
|
323
381
|
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
|
324
382
|
]
|
|
325
383
|
|
|
384
|
+
[[package]]
|
|
385
|
+
name = "icdiff"
|
|
386
|
+
version = "2.0.7"
|
|
387
|
+
source = { registry = "https://pypi.org/simple" }
|
|
388
|
+
sdist = { url = "https://files.pythonhosted.org/packages/fa/e4/43341832be5f2bcae71eb3ef08a07aaef9b74f74fe0b3675f62bd12057fe/icdiff-2.0.7.tar.gz", hash = "sha256:f79a318891adbf59a45e3a7694f5e1f18c5407065264637072ac8363b759866f", size = 16394, upload-time = "2023-08-21T15:00:55.742Z" }
|
|
389
|
+
wheels = [
|
|
390
|
+
{ url = "https://files.pythonhosted.org/packages/7c/2a/b3178baa75a3ec75a33588252296c82a1332d2b83cd01061539b74bde9dd/icdiff-2.0.7-py3-none-any.whl", hash = "sha256:f05d1b3623223dd1c70f7848da7d699de3d9a2550b902a8234d9026292fb5762", size = 17018, upload-time = "2023-08-21T15:00:54.634Z" },
|
|
391
|
+
]
|
|
392
|
+
|
|
326
393
|
[[package]]
|
|
327
394
|
name = "identify"
|
|
328
395
|
version = "2.6.15"
|
|
@@ -441,6 +508,15 @@ wheels = [
|
|
|
441
508
|
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
|
|
442
509
|
]
|
|
443
510
|
|
|
511
|
+
[[package]]
|
|
512
|
+
name = "mccabe"
|
|
513
|
+
version = "0.7.0"
|
|
514
|
+
source = { registry = "https://pypi.org/simple" }
|
|
515
|
+
sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" }
|
|
516
|
+
wheels = [
|
|
517
|
+
{ url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" },
|
|
518
|
+
]
|
|
519
|
+
|
|
444
520
|
[[package]]
|
|
445
521
|
name = "mdurl"
|
|
446
522
|
version = "0.1.2"
|
|
@@ -824,6 +900,24 @@ wheels = [
|
|
|
824
900
|
{ url = "https://files.pythonhosted.org/packages/91/ed/1e347d85d05b37a8b9a039ca832e5747e1e5248d0bd66042783ef48b4a37/puremagic-1.30-py3-none-any.whl", hash = "sha256:5eeeb2dd86f335b9cfe8e205346612197af3500c6872dffebf26929f56e9d3c1", size = 43304, upload-time = "2025-07-04T18:48:34.801Z" },
|
|
825
901
|
]
|
|
826
902
|
|
|
903
|
+
[[package]]
|
|
904
|
+
name = "pycodestyle"
|
|
905
|
+
version = "2.14.0"
|
|
906
|
+
source = { registry = "https://pypi.org/simple" }
|
|
907
|
+
sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472, upload-time = "2025-06-20T18:49:48.75Z" }
|
|
908
|
+
wheels = [
|
|
909
|
+
{ url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" },
|
|
910
|
+
]
|
|
911
|
+
|
|
912
|
+
[[package]]
|
|
913
|
+
name = "pyflakes"
|
|
914
|
+
version = "3.4.0"
|
|
915
|
+
source = { registry = "https://pypi.org/simple" }
|
|
916
|
+
sdist = { url = "https://files.pythonhosted.org/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669, upload-time = "2025-06-20T18:45:27.834Z" }
|
|
917
|
+
wheels = [
|
|
918
|
+
{ url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" },
|
|
919
|
+
]
|
|
920
|
+
|
|
827
921
|
[[package]]
|
|
828
922
|
name = "pygments"
|
|
829
923
|
version = "2.19.2"
|
|
@@ -1079,6 +1173,15 @@ wheels = [
|
|
|
1079
1173
|
{ url = "https://files.pythonhosted.org/packages/1d/d2/1637f4360ada6a368d3265bf39f2cf737a0aaab15ab520fc005903e883f8/ruff-0.14.7-py3-none-win_arm64.whl", hash = "sha256:be4d653d3bea1b19742fcc6502354e32f65cd61ff2fbdb365803ef2c2aec6228", size = 13609215, upload-time = "2025-11-28T20:55:15.375Z" },
|
|
1080
1174
|
]
|
|
1081
1175
|
|
|
1176
|
+
[[package]]
|
|
1177
|
+
name = "setuptools"
|
|
1178
|
+
version = "80.9.0"
|
|
1179
|
+
source = { registry = "https://pypi.org/simple" }
|
|
1180
|
+
sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" }
|
|
1181
|
+
wheels = [
|
|
1182
|
+
{ url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" },
|
|
1183
|
+
]
|
|
1184
|
+
|
|
1082
1185
|
[[package]]
|
|
1083
1186
|
name = "six"
|
|
1084
1187
|
version = "1.17.0"
|
|
@@ -1117,7 +1220,7 @@ wheels = [
|
|
|
1117
1220
|
|
|
1118
1221
|
[[package]]
|
|
1119
1222
|
name = "updates2mqtt"
|
|
1120
|
-
version = "1.4.
|
|
1223
|
+
version = "1.4.1"
|
|
1121
1224
|
source = { editable = "." }
|
|
1122
1225
|
dependencies = [
|
|
1123
1226
|
{ name = "docker" },
|
|
@@ -1133,6 +1236,8 @@ dependencies = [
|
|
|
1133
1236
|
[package.dev-dependencies]
|
|
1134
1237
|
dev = [
|
|
1135
1238
|
{ name = "coverage" },
|
|
1239
|
+
{ name = "genbadge", extra = ["all"] },
|
|
1240
|
+
{ name = "icdiff" },
|
|
1136
1241
|
{ name = "pre-commit" },
|
|
1137
1242
|
{ name = "pytest" },
|
|
1138
1243
|
{ name = "pytest-asyncio" },
|
|
@@ -1169,6 +1274,8 @@ requires-dist = [
|
|
|
1169
1274
|
[package.metadata.requires-dev]
|
|
1170
1275
|
dev = [
|
|
1171
1276
|
{ name = "coverage" },
|
|
1277
|
+
{ name = "genbadge", extras = ["all"] },
|
|
1278
|
+
{ name = "icdiff" },
|
|
1172
1279
|
{ name = "pre-commit", specifier = ">=4.4.0" },
|
|
1173
1280
|
{ name = "pytest", specifier = ">=8.4.2" },
|
|
1174
1281
|
{ name = "pytest-asyncio", specifier = ">=1.2.0" },
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|