Skip to content

Commit

Permalink
2024.9.0
Browse files Browse the repository at this point in the history
- Resolve async entry setup deprecated method
- Upgrade camera to use async aiohttp lib
- Resolve file issues with the Camera sensor
- Prevent error if less than required sensors are available from Strava API
  • Loading branch information
craibo committed Sep 15, 2024
1 parent 035d2e1 commit c0558f5
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 59 deletions.
12 changes: 7 additions & 5 deletions custom_components/ha_strava/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
EVENT_CORE_CONFIG_UPDATE,
EVENT_HOMEASSISTANT_START,
)

# HASS imports
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_entry_oauth2_flow
Expand Down Expand Up @@ -820,7 +821,9 @@ async def async_strava_config_update_handler():
return

def strava_config_update_handler(event): # pylint: disable=unused-argument
asyncio.run_coroutine_threadsafe(async_strava_config_update_handler(), hass.loop)
asyncio.run_coroutine_threadsafe(
async_strava_config_update_handler(), hass.loop
)

def core_config_update_handler(event):
"""
Expand Down Expand Up @@ -865,10 +868,9 @@ def core_config_update_handler(event):
entry.add_update_listener(strava_config_update_helper)
]

for component in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)
hass.async_create_task(
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
)

return True

Expand Down
103 changes: 52 additions & 51 deletions custom_components/ha_strava/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@

from __future__ import annotations

import io
import logging
import os
import pickle
from datetime import timedelta
from hashlib import md5

import aiofiles # pylint: disable=import-error
import requests
import aiohttp
from homeassistant.components.camera import Camera
from homeassistant.helpers.event import async_track_time_interval

Expand Down Expand Up @@ -113,31 +114,32 @@ def state(self): # pylint: disable=overridden-final-method
return _DEFAULT_IMAGE_URL
return self._urls[self._url_index]["url"]

def camera_image(
async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return image response."""
if len(self._urls) == 0:
_LOGGER.debug(f"{self._device_id}: serving default image")
return _return_default_img()
return await _return_default_img()

url = self._urls[self._url_index]["url"]
response = requests.get(url=url, timeout=60000)
if response.status_code != 200:
_LOGGER.error(
f"{self._device_id}: Invalid Image: {response.status_code}: {url}"
)
return _return_default_img()
return response.content
async with aiohttp.ClientSession() as session:
async with session.get(url=url, timeout=60) as response:
if response.status != 200:
_LOGGER.error(
f"{self._device_id}: Invalid Image: {response.status}: {url}"
)
return await _return_default_img()
return await response.read()

def rotate_img(self): # pylint: disable=missing-function-docstring
_LOGGER.debug(f"{self._device_id}: Strava Image Count: {len(self._urls)}")
if len(self._urls) == 0:
return
self._url_index = (self._url_index + 1) % len(self._urls)
self.schedule_update_ha_state()
self.async_write_ha_state()

def img_update_handler(self, event):
async def img_update_handler(self, event):
"""handle new urls of Strava images"""
_LOGGER.debug(f"{self._device_id}: Received image update: {event}")
if event.data["activity_index"] != self._activity_index:
Expand Down Expand Up @@ -189,13 +191,13 @@ async def _load_pickle_urls(self):
try:
async with aiofiles.open(self._url_dump_filepath, "rb") as file:
content = await file.read()
self._urls = pickle.loads(content)
self._urls = pickle.load(io.BytesIO(content))
except FileNotFoundError:
_LOGGER.error("File not found")
except pickle.UnpicklingError as pe:
_LOGGER.error(f"Invalid data in file: {pe}", exc_info=pe)
_LOGGER.error(f"Invalid data in file: {pe}")
except Exception as e: # pylint: disable=broad-exception-caught
_LOGGER.error(f"Error reading from file: {e}", exc_info=e)
_LOGGER.error(f"Error reading from file: {e}")

async def _store_pickle_urls(self):
"""store image urls persistently on hard drive"""
Expand All @@ -205,46 +207,45 @@ async def _store_pickle_urls(self):
except FileNotFoundError:
_LOGGER.error("File not found")
except pickle.PickleError as pe:
_LOGGER.error(f"Invalid data in file: {pe}", exc_info=pe)
_LOGGER.error(f"Invalid data in file: {pe}")
except Exception as e: # pylint: disable=broad-exception-caught
_LOGGER.error(f"Error storing images to file: {e}", exc_info=e)
_LOGGER.error(f"Error storing images to file: {e}")

def _return_default_img(self):
img_response = requests.get( # pylint: disable=unused-argument
url=self._default_url, timeout=60000
)
return img_response.content
async def _return_default_img(self):
async with aiohttp.ClientSession() as session:
async with session.get(url=self._default_url, timeout=60) as img_response:
return await img_response.read()

def is_url_valid(self, url):
async def is_url_valid(self, url):
"""test whether an image URL returns a valid response"""
img_response = requests.get( # pylint: disable=unused-argument
url=url, timeout=60000
)
if img_response.status_code == 200:
return True
_LOGGER.error(
f"{url} did not return a valid image | Response: {img_response.status_code}"
)
return False

def camera_image(
async with aiohttp.ClientSession() as session:
async with session.get(url=url, timeout=60) as img_response:
if img_response.status == 200:
return True
_LOGGER.error(
f"{url} did not return a valid image | Response: {img_response.status}"
)
return False

async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return image response."""
if len(self._urls) == self._url_index:
_LOGGER.debug("No custom image urls....serving default image")
return self._return_default_img()

img_response = requests.get( # pylint: disable=unused-argument
url=self._urls[list(self._urls.keys())[self._url_index]]["url"],
timeout=60000,
)
if img_response.status_code == 200:
return img_response.content
_LOGGER.error(
f"{self._urls[list(self._urls.keys())[self._url_index]]['url']} did not return a valid image. Response: {img_response.status_code}" # noqa: E501
)
return self._return_default_img()
return await self._return_default_img()

async with aiohttp.ClientSession() as session:
async with session.get( # pylint: disable=unused-argument
url=self._urls[list(self._urls.keys())[self._url_index]]["url"],
timeout=60,
) as img_response:
if img_response.status == 200:
return await img_response.read()
_LOGGER.error(
f"{self._urls[list(self._urls.keys())[self._url_index]]['url']} did not return a valid image. Response: {img_response.status}" # noqa: E501
)
return await self._return_default_img()

def rotate_img(self): # pylint: disable=missing-function-docstring
_LOGGER.debug(f"Number of images available from Strava: {len(self._urls)}")
Expand Down Expand Up @@ -273,7 +274,7 @@ async def img_update_handler(self, event):

# Append new images to the urls dict, keyed by url hash.
for img_url in event.data["img_urls"]:
if self.is_url_valid(url=img_url["url"]):
if await self.is_url_valid(url=img_url["url"]):
self._urls[md5(img_url["url"].encode()).hexdigest()] = {**img_url}

# Ensure the urls dict is sorted by date and truncated to max # images.
Expand All @@ -299,7 +300,7 @@ async def async_will_remove_from_hass(self):
await super().async_will_remove_from_hass()


def _return_default_img():
return requests.get( # pylint: disable=unused-argument
url=_DEFAULT_IMAGE_URL, timeout=60000
).content
async def _return_default_img():
async with aiohttp.ClientSession() as session:
async with session.get(url=_DEFAULT_IMAGE_URL, timeout=60) as img_response:
return await img_response.read()
14 changes: 11 additions & 3 deletions custom_components/ha_strava/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@ def native_value(self):

if metric == CONF_SENSOR_DISTANCE:
distance = self._data[CONF_SENSOR_DISTANCE] / 1000
if self._is_unit_metric_default or self._is_unit_metric:
if self._is_unit_metric_default or self._is_unit_metric:
return round(distance, 2)
return round(
DistanceConverter.convert(
Expand Down Expand Up @@ -720,8 +720,16 @@ def get_metric(self):

def strava_data_update_event_handler(self, event):
"""Handle Strava API data which is emitted from a Strava Update Event"""
self._data = event.data["activities"][self._activity_index]
self.schedule_update_ha_state()
activities = event.data.get("activities", [])
if 0 <= self._activity_index < len(activities):
self._data = activities[self._activity_index]
self.schedule_update_ha_state()
else:
_LOGGER.info(
"Invalid activity index: %s. Number of activities returned from Strava API: %s",
self._activity_index + 1,
len(activities),
)

async def async_added_to_hass(self):
self.hass.bus.async_listen(
Expand Down

0 comments on commit c0558f5

Please sign in to comment.