From 0a0cb4fa169dc9743e3e89170f2d42fa08a13b30 Mon Sep 17 00:00:00 2001 From: Yoav Mor Date: Wed, 30 Sep 2026 09:45:01 +0300 Subject: [PATCH] Add latest release downloads sensor to GitHub (#183679) --- .../components/github/coordinator.py | 5 ++ homeassistant/components/github/icons.json | 3 + homeassistant/components/github/sensor.py | 10 ++++ homeassistant/components/github/strings.json | 4 ++ tests/components/github/fixtures/graphql.json | 3 +- .../github/snapshots/test_diagnostics.ambr | 7 +++ .../github/snapshots/test_sensor.ambr | 55 +++++++++++++++++++ tests/components/github/test_sensor.py | 14 +++++ 8 files changed, 100 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/github/coordinator.py b/homeassistant/components/github/coordinator.py index 29ff49c5c5ac..2566a3a84f21 100644 --- a/homeassistant/components/github/coordinator.py +++ b/homeassistant/components/github/coordinator.py @@ -88,6 +88,11 @@ query ($owner: String!, $repository: String!) { name url tag: tagName + assets: releaseAssets(first: 100) { + nodes { + download_count: downloadCount + } + } } refs( first: 1 diff --git a/homeassistant/components/github/icons.json b/homeassistant/components/github/icons.json index 5e684ab19d2b..f23528ed3741 100644 --- a/homeassistant/components/github/icons.json +++ b/homeassistant/components/github/icons.json @@ -31,6 +31,9 @@ "latest_release": { "default": "mdi:github" }, + "latest_release_downloads": { + "default": "mdi:download" + }, "latest_tag": { "default": "mdi:tag" }, diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index 490fae3e4070..be46b625a74e 100644 --- a/homeassistant/components/github/sensor.py +++ b/homeassistant/components/github/sensor.py @@ -86,6 +86,16 @@ SENSOR_DESCRIPTIONS: tuple[GitHubSensorEntityDescription, ...] = ( state_class=SensorStateClass.TOTAL, value_fn=lambda data: data["merged_pull_request"]["total"], ), + GitHubSensorEntityDescription( + key="latest_release_downloads", + translation_key="latest_release_downloads", + entity_category=EntityCategory.DIAGNOSTIC, + state_class=SensorStateClass.TOTAL_INCREASING, + avabl_fn=lambda data: data["release"] and data["release"]["assets"]["nodes"], + value_fn=lambda data: sum( + asset["download_count"] for asset in data["release"]["assets"]["nodes"] + ), + ), GitHubSensorEntityDescription( key="latest_commit", translation_key="latest_commit", diff --git a/homeassistant/components/github/strings.json b/homeassistant/components/github/strings.json index 868f90f46a07..57feea813b63 100644 --- a/homeassistant/components/github/strings.json +++ b/homeassistant/components/github/strings.json @@ -67,6 +67,10 @@ "latest_release": { "name": "Latest release" }, + "latest_release_downloads": { + "name": "Latest release downloads", + "unit_of_measurement": "downloads" + }, "latest_tag": { "name": "Latest tag" }, diff --git a/tests/components/github/fixtures/graphql.json b/tests/components/github/fixtures/graphql.json index adb86d2a9e81..d3256c291beb 100644 --- a/tests/components/github/fixtures/graphql.json +++ b/tests/components/github/fixtures/graphql.json @@ -55,7 +55,8 @@ "release": { "name": "v1.0.0", "url": "https://github.com/octocat/Hello-World/releases/v1.0.0", - "tag": "v1.0.0" + "tag": "v1.0.0", + "assets": { "nodes": [{ "download_count": 42 }] } }, "refs": { "tags": [ diff --git a/tests/components/github/snapshots/test_diagnostics.ambr b/tests/components/github/snapshots/test_diagnostics.ambr index 00f78529e5d3..36bae527f15a 100644 --- a/tests/components/github/snapshots/test_diagnostics.ambr +++ b/tests/components/github/snapshots/test_diagnostics.ambr @@ -65,6 +65,13 @@ ]), }), 'release': dict({ + 'assets': dict({ + 'nodes': list([ + dict({ + 'download_count': 42, + }), + ]), + }), 'name': 'v1.0.0', 'tag': 'v1.0.0', 'url': 'https://github.com/octocat/Hello-World/releases/v1.0.0', diff --git a/tests/components/github/snapshots/test_sensor.ambr b/tests/components/github/snapshots/test_sensor.ambr index 36581e8c7504..12a670c28c52 100644 --- a/tests/components/github/snapshots/test_sensor.ambr +++ b/tests/components/github/snapshots/test_sensor.ambr @@ -537,6 +537,61 @@ 'state': 'v1.0.0', }) # --- +# name: test_all_entities[sensor.octocat_hello_world_latest_release_downloads-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.octocat_hello_world_latest_release_downloads', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Latest release downloads', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Latest release downloads', + 'platform': 'github', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'latest_release_downloads', + 'unique_id': '1296269_latest_release_downloads', + 'unit_of_measurement': 'downloads', + }) +# --- +# name: test_all_entities[sensor.octocat_hello_world_latest_release_downloads-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Data provided by the GitHub API', + : 'octocat/Hello-World Latest release downloads', + : , + : 'downloads', + }), + 'context': , + 'entity_id': 'sensor.octocat_hello_world_latest_release_downloads', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '42', + }) +# --- # name: test_all_entities[sensor.octocat_hello_world_latest_tag-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ diff --git a/tests/components/github/test_sensor.py b/tests/components/github/test_sensor.py index 466b545b6e4c..bc1885170744 100644 --- a/tests/components/github/test_sensor.py +++ b/tests/components/github/test_sensor.py @@ -51,3 +51,17 @@ async def test_sensor_updates_with_empty_release_array( new_state = hass.states.get(TEST_SENSOR_ENTITY) assert new_state.state == STATE_UNAVAILABLE + + +async def test_latest_release_downloads_without_assets( + hass: HomeAssistant, + github_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the downloads sensor is unavailable when the release has no assets.""" + release = github_client.graphql.return_value.data["data"]["repository"]["release"] + release["assets"]["nodes"] = [] + await setup_integration(hass, mock_config_entry) + + state = hass.states.get("sensor.octocat_hello_world_latest_release_downloads") + assert state.state == STATE_UNAVAILABLE