From e894e207051eca2d75791cd07dd6bcfc563d5d11 Mon Sep 17 00:00:00 2001 From: Aniket <58530748+HandyHat@users.noreply.github.com> Date: Sat, 14 Jan 2023 13:36:13 +0000 Subject: [PATCH] V1.0.3 (#296) * Update error handling * Remove unused variables Also add additional typing * Update pyglowmarkt dependency A new version has now been published * Bump version * Remove advanced codeql workflow * Consolidate workflows * Change some debug logging to warning --- .github/workflows/codeql-analysis.yml | 71 ------- .github/workflows/hassfest.yaml | 16 -- .github/workflows/validate.yaml | 19 -- .github/workflows/validate.yml | 23 +++ .../hildebrandglow_dcc/manifest.json | 4 +- .../hildebrandglow_dcc/sensor.py | 193 +++++++----------- 6 files changed, 103 insertions(+), 223 deletions(-) delete mode 100644 .github/workflows/codeql-analysis.yml delete mode 100644 .github/workflows/hassfest.yaml delete mode 100644 .github/workflows/validate.yaml create mode 100644 .github/workflows/validate.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 32924b7..0000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,71 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - branches: [ main ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ main ] - schedule: - - cron: '44 19 * * 6' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'python' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] - # Learn more: - # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - - # ℹī¸ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/hassfest.yaml b/.github/workflows/hassfest.yaml deleted file mode 100644 index 1c86cd5..0000000 --- a/.github/workflows/hassfest.yaml +++ /dev/null @@ -1,16 +0,0 @@ -name: Validate with hassfest - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - schedule: - - cron: "0 0 * * *" - -jobs: - validate: - runs-on: "ubuntu-latest" - steps: - - uses: "actions/checkout@v3" - - uses: home-assistant/actions/hassfest@master diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml deleted file mode 100644 index 5d19105..0000000 --- a/.github/workflows/validate.yaml +++ /dev/null @@ -1,19 +0,0 @@ -name: Validate with HACS - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - schedule: - - cron: "0 0 * * *" - -jobs: - validate: - runs-on: "ubuntu-latest" - steps: - - uses: "actions/checkout@v3" - - name: HACS validation - uses: "hacs/action@main" - with: - category: "integration" \ No newline at end of file diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..32761db --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,23 @@ +name: Validate + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + schedule: + - cron: "27 0 * * 6" + workflow_dispatch: + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - name: Checkout the repository + uses: actions/checkout@v3.3.0 + - name: HACS Validation + uses: "hacs/action@22.5.0" + with: + category: "integration" + - name: Hassfest Validation + uses: home-assistant/actions/hassfest@master \ No newline at end of file diff --git a/custom_components/hildebrandglow_dcc/manifest.json b/custom_components/hildebrandglow_dcc/manifest.json index a4835de..a25c338 100644 --- a/custom_components/hildebrandglow_dcc/manifest.json +++ b/custom_components/hildebrandglow_dcc/manifest.json @@ -6,11 +6,11 @@ "documentation": "https://github.com/HandyHat/ha-hildebrandglow-dcc", "issue_tracker": "https://github.com/HandyHat/ha-hildebrandglow-dcc/issues", "requirements": [ - "https://github.com/cybermaggedon/pyglowmarkt/archive/69e9c949a3f0d0293ad1af4005de154b5de04234.zip#pyglowmarkt==0.5.4" + "pyglowmarkt==0.5.5" ], "codeowners": [ "@HandyHat" ], "iot_class": "cloud_polling", - "version": "1.0.2" + "version": "1.0.3" } \ No newline at end of file diff --git a/custom_components/hildebrandglow_dcc/sensor.py b/custom_components/hildebrandglow_dcc/sensor.py index 98a5f53..041e5e1 100644 --- a/custom_components/hildebrandglow_dcc/sensor.py +++ b/custom_components/hildebrandglow_dcc/sensor.py @@ -5,7 +5,6 @@ from datetime import datetime, time, timedelta import logging -from glowmarkt import BrightClient import requests from homeassistant.components.sensor import ( @@ -16,7 +15,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfEnergy from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -33,8 +31,8 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable ) -> bool: """Set up the sensor platform.""" - entities = [] - meters = {} + entities: list = [] + meters: dict = {} # Get API object from the config flow glowmarkt = hass.data[DOMAIN][entry.entry_id] @@ -45,57 +43,66 @@ async def async_setup_entry( virtual_entities = await hass.async_add_executor_job( glowmarkt.get_virtual_entities ) + _LOGGER.debug("Successful GET to %svirtualentity", glowmarkt.url) except requests.Timeout as ex: _LOGGER.error("Timeout: %s", ex) except requests.exceptions.ConnectionError as ex: _LOGGER.error("Cannot connect: %s", ex) # Can't use the RuntimeError exception from the library as it's not a subclass of Exception except Exception as ex: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception: %s. Please open an issue", ex) - _LOGGER.debug("Successful GET to %svirtualentity", glowmarkt.url) + if "Request failed" in str(ex): + _LOGGER.error( + "Non-200 Status Code. The Glow API may be experiencing issues" + ) + else: + _LOGGER.exception("Unexpected exception: %s. Please open an issue", ex) for virtual_entity in virtual_entities: # Gather all resources for each virtual entity resources: dict = {} try: resources = await hass.async_add_executor_job(virtual_entity.get_resources) + _LOGGER.debug( + "Successful GET to %svirtualentity/%s/resources", + glowmarkt.url, + virtual_entity.id, + ) except requests.Timeout as ex: _LOGGER.error("Timeout: %s", ex) except requests.exceptions.ConnectionError as ex: _LOGGER.error("Cannot connect: %s", ex) # Can't use the RuntimeError exception from the library as it's not a subclass of Exception except Exception as ex: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception: %s. Please open an issue", ex) - _LOGGER.debug( - "Successful GET to %svirtualentity/%s/resources", - glowmarkt.url, - virtual_entity.id, - ) + if "Request failed" in str(ex): + _LOGGER.error( + "Non-200 Status Code. The Glow API may be experiencing issues" + ) + else: + _LOGGER.exception("Unexpected exception: %s. Please open an issue", ex) # Loop through all resources and create sensors for resource in resources: if resource.classifier in ["electricity.consumption", "gas.consumption"]: - usage_sensor = Usage(hass, resource, virtual_entity, entry) + usage_sensor = Usage(hass, resource, virtual_entity) entities.append(usage_sensor) # Save the usage sensor as a meter so that the cost sensor can reference it meters[resource.classifier] = usage_sensor # Standing and Rate sensors are handled by the coordinator - coordinator = TariffCoordinator(hass, glowmarkt, resource, entry) + coordinator = TariffCoordinator(hass, resource) standing_sensor = Standing(coordinator, resource, virtual_entity) entities.append(standing_sensor) rate_sensor = Rate(coordinator, resource, virtual_entity) entities.append(rate_sensor) - # await coordinator.async_config_entry_first_refresh() # This isn't necessary as update_before_add is already specified # Cost sensors must be created after usage sensors as they reference them as a meter for resource in resources: if resource.classifier == "gas.consumption.cost": - cost_sensor = Cost(hass, resource, virtual_entity, entry) + cost_sensor = Cost(hass, resource, virtual_entity) cost_sensor.meter = meters["gas.consumption"] entities.append(cost_sensor) elif resource.classifier == "electricity.consumption.cost": - cost_sensor = Cost(hass, resource, virtual_entity, entry) + cost_sensor = Cost(hass, resource, virtual_entity) cost_sensor.meter = meters["electricity.consumption"] entities.append(cost_sensor) @@ -134,7 +141,7 @@ async def should_update() -> bool: return False -async def daily_data(self) -> float: +async def daily_data(hass: HomeAssistant, resource) -> float: """Get daily usage from the API.""" # If it's before 01:06, we need to fetch yesterday's data # Should only need to be before 00:36 but gas data can be 30 minutes behind electricity data @@ -144,13 +151,17 @@ async def daily_data(self) -> float: else: now = datetime.now() # Round to the day to set time to 00:00:00 - t_from = await self.hass.async_add_executor_job(self.resource.round, now, "P1D") + t_from = await hass.async_add_executor_job(resource.round, now, "P1D") # Round to the minute - t_to = await self.hass.async_add_executor_job(self.resource.round, now, "PT1M") + t_to = await hass.async_add_executor_job(resource.round, now, "PT1M") # Tell Hildebrand to pull latest DCC data try: - await self.hass.async_add_executor_job(self.resource.catchup) + await hass.async_add_executor_job(resource.catchup) + _LOGGER.debug( + "Successful GET to https://api.glowmarkt.com/api/v0-1/resource/%s/catchup", + resource.id, + ) except requests.Timeout as ex: _LOGGER.error("Timeout: %s", ex) except requests.exceptions.ConnectionError as ex: @@ -158,29 +169,18 @@ async def daily_data(self) -> float: # Can't use the RuntimeError exception from the library as it's not a subclass of Exception except Exception as ex: # pylint: disable=broad-except if "Request failed" in str(ex): - _LOGGER.debug("Non-200 Status Code. Refreshing auth") - await refresh_token(self) - try: - await self.hass.async_add_executor_job(self.resource.catchup) - except requests.Timeout as secondary_ex: - _LOGGER.error("Timeout: %s", secondary_ex) - except requests.exceptions.ConnectionError as secondary_ex: - _LOGGER.error("Cannot connect: %s", secondary_ex) - except Exception as secondary_ex: # pylint: disable=broad-except - _LOGGER.exception( - "Unexpected exception: %s. Please open an issue", secondary_ex - ) + _LOGGER.warning( + "Non-200 Status Code. The Glow API may be experiencing issues" + ) else: _LOGGER.exception("Unexpected exception: %s. Please open an issue", ex) - _LOGGER.debug( - "Successful GET to https://api.glowmarkt.com/api/v0-1/resource/%s/catchup", - self.resource.id, - ) try: - readings = await self.hass.async_add_executor_job( - self.resource.get_readings, t_from, t_to, "P1D", "sum", True + readings = await hass.async_add_executor_job( + resource.get_readings, t_from, t_to, "P1D", "sum", True ) + _LOGGER.debug("Successfully got daily usage for resource id %s", resource.id) + return readings[0][1].value except requests.Timeout as ex: _LOGGER.error("Timeout: %s", ex) except requests.exceptions.ConnectionError as ex: @@ -188,42 +188,29 @@ async def daily_data(self) -> float: # Can't use the RuntimeError exception from the library as it's not a subclass of Exception except Exception as ex: # pylint: disable=broad-except if "Request failed" in str(ex): - _LOGGER.debug("Non-200 Status Code. Refreshing auth") - await refresh_token(self) - try: - readings = await self.hass.async_add_executor_job( - self.resource.get_readings, t_from, t_to, "P1D", "sum", True - ) - except requests.Timeout as secondary_ex: - _LOGGER.error("Timeout: %s", secondary_ex) - except requests.exceptions.ConnectionError as secondary_ex: - _LOGGER.error("Cannot connect: %s", secondary_ex) - except Exception as secondary_ex: # pylint: disable=broad-except - _LOGGER.exception( - "Unexpected exception: %s. Please open an issue", secondary_ex - ) + _LOGGER.warning( + "Non-200 Status Code. The Glow API may be experiencing issues" + ) else: _LOGGER.exception("Unexpected exception: %s. Please open an issue", ex) - _LOGGER.debug("Successfully got daily usage for resource id %s", self.resource.id) - return readings[0][1].value + return None -async def tariff_data(self) -> float: +async def tariff_data(hass: HomeAssistant, resource) -> float: """Get tariff data from the API.""" try: - tariff = await self.hass.async_add_executor_job(self.resource.get_tariff) + tariff = await hass.async_add_executor_job(resource.get_tariff) _LOGGER.debug( - "Successful GET to %sresource/%s/tariff", - self.glowmarkt.url, - self.resource.id, + "Successful GET to https://api.glowmarkt.com/api/v0-1/resource/%s/tariff", + resource.id, ) return tariff except UnboundLocalError: - supply = supply_type(self.resource) + supply = supply_type(resource) _LOGGER.warning( "No tariff data found for %s meter (id: %s). If you don't see tariff data for this meter in the Bright app, please disable the associated rate and standing charge sensors", supply, - self.resource.id, + resource.id, ) except requests.Timeout as ex: _LOGGER.error("Timeout: %s", ex) @@ -231,36 +218,15 @@ async def tariff_data(self) -> float: _LOGGER.error("Cannot connect: %s", ex) # Can't use the RuntimeError exception from the library as it's not a subclass of Exception except Exception as ex: # pylint: disable=broad-except - if "Request failed" in str(ex) and not self.auth_refreshed: - _LOGGER.debug("Non-200 Status Code. Refreshing auth") - await refresh_token(self) - self.auth_refreshed = True - await tariff_data(self) - _LOGGER.exception("Unexpected exception: %s. Please open an issue", ex) + if "Request failed" in str(ex): + _LOGGER.warning( + "Non-200 Status Code. The Glow API may be experiencing issues" + ) + else: + _LOGGER.exception("Unexpected exception: %s. Please open an issue", ex) return None -async def refresh_token(self): - """Refresh the glowmarkt API token""" - try: - glowmarkt = await self.hass.async_add_executor_job( - BrightClient, self.entry.data["username"], self.entry.data["password"] - ) - except requests.Timeout as ex: - raise ConfigEntryNotReady(f"Timeout: {ex}") from ex - except requests.exceptions.ConnectionError as ex: - raise ConfigEntryNotReady(f"Cannot connect: {ex}") from ex - except Exception as ex: # pylint: disable=broad-except - raise ConfigEntryNotReady( - f"Unexpected exception: {ex}. Please open an issue." - ) from ex - else: - _LOGGER.debug("Successful Post to %sauth", glowmarkt.url) - - # Set API object - self.hass.data[DOMAIN][self.entry.entry_id] = glowmarkt - - class Usage(SensorEntity): """Sensor object for daily usage.""" @@ -270,13 +236,10 @@ class Usage(SensorEntity): _attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR _attr_state_class = SensorStateClass.TOTAL_INCREASING - def __init__( - self, hass: HomeAssistant, resource, virtual_entity, entry: ConfigEntry - ) -> None: + def __init__(self, hass: HomeAssistant, resource, virtual_entity) -> None: """Initialize the sensor.""" self._attr_unique_id = resource.id - self.entry = entry self.hass = hass self.initialised = False self.resource = resource @@ -303,14 +266,16 @@ async def async_update(self) -> None: """Fetch new data for the sensor.""" # Get data on initial startup if not self.initialised: - value = await daily_data(self) - self._attr_native_value = round(value, 2) - self.initialised = True + value = await daily_data(self.hass, self.resource) + if value: + self._attr_native_value = round(value, 2) + self.initialised = True else: # Only update the sensor if it's between 0-5 or 30-35 minutes past the hour if await should_update(): - value = await daily_data(self) - self._attr_native_value = round(value, 2) + value = await daily_data(self.hass, self.resource) + if value: + self._attr_native_value = round(value, 2) class Cost(SensorEntity): @@ -322,13 +287,10 @@ class Cost(SensorEntity): _attr_native_unit_of_measurement = "GBP" _attr_state_class = SensorStateClass.TOTAL_INCREASING - def __init__( - self, hass: HomeAssistant, resource, virtual_entity, entry: ConfigEntry - ) -> None: + def __init__(self, hass: HomeAssistant, resource, virtual_entity) -> None: """Initialize the sensor.""" self._attr_unique_id = resource.id - self.entry = entry self.hass = hass self.initialised = False self.meter = None @@ -349,20 +311,22 @@ def device_info(self) -> DeviceInfo: async def async_update(self) -> None: """Fetch new data for the sensor.""" if not self.initialised: - value = await daily_data(self) / 100 - self._attr_native_value = round(value, 2) - self.initialised = True + value = await daily_data(self.hass, self.resource) + if value: + self._attr_native_value = round(value / 100, 2) + self.initialised = True else: # Only update the sensor if it's between 0-5 or 30-35 minutes past the hour if await should_update(): - value = await daily_data(self) / 100 - self._attr_native_value = round(value, 2) + value = await daily_data(self.hass, self.resource) + if value: + self._attr_native_value = round(value / 100, 2) class TariffCoordinator(DataUpdateCoordinator): """Data update coordinator for the tariff sensors.""" - def __init__(self, hass: HomeAssistant, glowmarkt, resource, entry: ConfigEntry): + def __init__(self, hass: HomeAssistant, resource) -> None: """Initialize tariff coordinator.""" super().__init__( hass, @@ -372,8 +336,7 @@ def __init__(self, hass: HomeAssistant, glowmarkt, resource, entry: ConfigEntry) # Polling interval. Will only be polled if there are subscribers. update_interval=timedelta(minutes=5), ) - self.entry = entry - self.glowmarkt = glowmarkt + self.rate_initialised = False self.standing_initialised = False self.resource = resource @@ -384,12 +347,12 @@ async def _async_update_data(self): if not self.standing_initialised: if not self.rate_initialised: self.rate_initialised = True - return await tariff_data(self) + return await tariff_data(self.hass, self.resource) self.standing_initialised = True - return await tariff_data(self) + return await tariff_data(self.hass, self.resource) # Only poll when updated data might be available if await should_update(): - tariff = await tariff_data(self) + tariff = await tariff_data(self.hass, self.resource) return tariff @@ -412,7 +375,7 @@ class Standing(CoordinatorEntity, SensorEntity): False # Don't enable by default as less commonly used ) - def __init__(self, coordinator, resource, virtual_entity): + def __init__(self, coordinator, resource, virtual_entity) -> None: """Pass coordinator to CoordinatorEntity.""" super().__init__(coordinator) @@ -464,7 +427,7 @@ class Rate(CoordinatorEntity, SensorEntity): False # Don't enable by default as less commonly used ) - def __init__(self, coordinator, resource, virtual_entity): + def __init__(self, coordinator, resource, virtual_entity) -> None: """Pass coordinator to CoordinatorEntity.""" super().__init__(coordinator)