Skip to content

Commit

Permalink
Remove forecastio dependency & Fire Index value sensor (#326)
Browse files Browse the repository at this point in the history
Co-authored-by: Alexander Rey <[email protected]>
Co-authored-by: alexander0042 <[email protected]>
  • Loading branch information
3 people authored Oct 8, 2024
1 parent 7ad4a55 commit 2bc308f
Show file tree
Hide file tree
Showing 5 changed files with 206 additions and 7 deletions.
1 change: 1 addition & 0 deletions custom_components/pirateweather/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@
"time": "Time",
"fire_index": "Fire Index",
"fire_index_max": "Fire Index Max",
"fire_risk_level": "Fire Risk Level",
"smoke": "Smoke",
"smoke_max": "Smoke Max",
"liquid_accumulation": "Liquid Accumulation",
Expand Down
163 changes: 163 additions & 0 deletions custom_components/pirateweather/forecast_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
"""Taken from the fantastic Dark Sky library: https://github.com/ZeevG/python-forecast.io in October 2024."""

import datetime

import requests


class UnicodeMixin:
"""Provide string representation for Python 2/3 compatibility."""

def __str__(self):
"""Return the unicode representation of the object for Python 2/3 compatibility."""
return self.__unicode__()


class PropertyUnavailable(AttributeError):
"""Raise when a requested property is unavailable in the forecast data."""


class Forecast(UnicodeMixin):
"""Represent the forecast data and provide methods to access weather blocks."""

def __init__(self, data, response, headers):
"""Initialize the Forecast with data, HTTP response, and headers."""
self.response = response
self.http_headers = headers
self.json = data

self._alerts = []
for alertJSON in self.json.get("alerts", []):
self._alerts.append(Alert(alertJSON))

def update(self):
"""Update the forecast data by making a new request to the same URL."""
r = requests.get(self.response.url)
self.json = r.json()
self.response = r

def currently(self):
"""Return the current weather data block."""
return self._forcastio_data("currently")

def minutely(self):
"""Return the minutely weather data block."""
return self._forcastio_data("minutely")

def hourly(self):
"""Return the hourly weather data block."""
return self._forcastio_data("hourly")

def daily(self):
"""Return the daily weather data block."""
return self._forcastio_data("daily")

def offset(self):
"""Return the time zone offset for the forecast location."""
return self.json["offset"]

def alerts(self):
"""Return the list of alerts issued for this forecast."""
return self._alerts

def _forcastio_data(self, key):
"""Fetch and return specific weather data (currently, minutely, hourly, daily)."""
keys = ["minutely", "currently", "hourly", "daily"]
try:
if key not in self.json:
keys.remove(key)
url = "{}&exclude={}{}".format(
self.response.url.split("&")[0],
",".join(keys),
",alerts,flags",
)

response = requests.get(url).json()
self.json[key] = response[key]

if key == "currently":
return ForecastioDataPoint(self.json[key])
return ForecastioDataBlock(self.json[key])
except requests.HTTPError:
if key == "currently":
return ForecastioDataPoint()
return ForecastioDataBlock()


class ForecastioDataBlock(UnicodeMixin):
"""Represent a block of weather data such as minutely, hourly, or daily summaries."""

def __init__(self, d=None):
"""Initialize the data block with summary and icon information."""
d = d or {}
self.summary = d.get("summary")
self.icon = d.get("icon")
self.data = [ForecastioDataPoint(datapoint) for datapoint in d.get("data", [])]

def __unicode__(self):
"""Return a string representation of the data block."""
return "<ForecastioDataBlock instance: " "%s with %d ForecastioDataPoints>" % (
self.summary,
len(self.data),
)


class ForecastioDataPoint(UnicodeMixin):
"""Represent a single data point in a weather forecast, such as an hourly or daily data point."""

def __init__(self, d={}):
"""Initialize the data point with timestamp and weather information."""
self.d = d

try:
self.time = datetime.datetime.fromtimestamp(int(d["time"]))
self.utime = d["time"]
except KeyError:
pass

try:
sr_time = int(d["sunriseTime"])
self.sunriseTime = datetime.datetime.fromtimestamp(sr_time)
except KeyError:
self.sunriseTime = None

try:
ss_time = int(d["sunsetTime"])
self.sunsetTime = datetime.datetime.fromtimestamp(ss_time)
except KeyError:
self.sunsetTime = None

def __getattr__(self, name):
"""Return the weather property dynamically or raise PropertyUnavailable if missing."""
try:
return self.d[name]
except KeyError as err:
raise PropertyUnavailable(
f"Property '{name}' is not valid"
" or is not available for this forecast"
) from err

def __unicode__(self):
"""Return a string representation of the data point."""
return "<ForecastioDataPoint instance: " f"{self.summary} at {self.time}>"


class Alert(UnicodeMixin):
"""Represent a weather alert, such as a storm warning or flood alert."""

def __init__(self, json):
"""Initialize the alert with the raw JSON data."""
self.json = json

def __getattr__(self, name):
"""Return the alert property dynamically or raise PropertyUnavailable if missing."""
try:
return self.json[name]
except KeyError as err:
raise PropertyUnavailable(
f"Property '{name}' is not valid" " or is not available for this alert"
) from err

def __unicode__(self):
"""Return a string representation of the alert."""
return f"<Alert instance: {self.title} at {self.time}>"
5 changes: 1 addition & 4 deletions custom_components/pirateweather/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,5 @@
"documentation": "https://github.com/alexander0042/pirate-weather-ha",
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/alexander0042/pirate-weather-ha/issues",
"requirements": [
"python-forecastio==1.4.0"
],
"version": "1.5.9"
"version": "1.6.0"
}
42 changes: 40 additions & 2 deletions custom_components/pirateweather/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,14 @@ class PirateWeatherSensorEntityDescription(SensorEntityDescription):
icon="mdi:fire",
forecast_mode=["currently", "hourly"],
),
"fire_risk_level": PirateWeatherSensorEntityDescription(
key="fire_risk_level",
name="Fire Risk Level",
device_class=SensorDeviceClass.ENUM,
icon="mdi:fire",
forecast_mode=["currently", "hourly", "daily"],
options=["Extreme", "Very High", "High", "Moderate", "Low", "N/A"],
),
"fire_index_max": PirateWeatherSensorEntityDescription(
key="fire_index_max",
name="Fire Index Max",
Expand Down Expand Up @@ -1125,8 +1133,17 @@ def get_state(self, data):
If the sensor type is unknown, the current state is returned.
"""
lookup_type = convert_to_camel(self.type)
state = data.get(lookup_type)

if self.type == "fire_risk_level":
if self.forecast_hour is not None:
state = data.get("fireIndex")
elif self.forecast_day is not None:
state = data.get("fireIndexMax")
else:
state = data.get("fireIndex")
else:
lookup_type = convert_to_camel(self.type)
state = data.get(lookup_type)

if state is None:
return state
Expand Down Expand Up @@ -1214,6 +1231,8 @@ def get_state(self, data):
]:
outState = datetime.datetime.fromtimestamp(state, datetime.UTC)

elif self.type == "fire_risk_level":
outState = fire_index(state)
elif self.type in [
"dew_point",
"temperature",
Expand Down Expand Up @@ -1277,3 +1296,22 @@ def convert_to_camel(data):
components = data.split("_")
capital_components = "".join(x.title() for x in components[1:])
return f"{components[0]}{capital_components}"


def fire_index(fire_index):
"""Convert numeric fire index to a textual value."""

if fire_index == -999:
outState = "N/A"
elif fire_index >= 30:
outState = "Extreme"
elif fire_index >= 20:
outState = "Very High"
elif fire_index >= 10:
outState = "High"
elif fire_index >= 5:
outState = "Moderate"
else:
outState = "Low"

return outState
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@

import aiohttp
import async_timeout
from forecastio.models import Forecast
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .const import (
DOMAIN,
)
from .forecast_models import Forecast

_LOGGER = logging.getLogger(__name__)

Expand Down

0 comments on commit 2bc308f

Please sign in to comment.