add 1 and 2 day predictions, fix attributes, set mqtt feeder interval to 15 min, rename cards

This commit is contained in:
Cyberes 2024-08-19 14:03:43 -06:00
parent 9493efa60f
commit 02fc826fb3
6 changed files with 117 additions and 120 deletions

View File

@ -9,24 +9,34 @@ from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_URL = "https://services.swpc.noaa.gov/products/noaa-scales.json" CONF_URL = "https://services.swpc.noaa.gov/products/noaa-scales.json"
SCAN_INTERVAL = timedelta(minutes=30) SCAN_INTERVAL = timedelta(minutes=5)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
session = async_get_clientsession(hass) session = async_get_clientsession(hass)
async_add_entities([ async_add_entities([
SpaceWeatherScaleSensor(session, CONF_URL, "R", '0', ''), SpaceWeatherScaleSensor(session, CONF_URL, "R", '0', None),
SpaceWeatherScaleSensor(session, CONF_URL, "S", '0', ''), SpaceWeatherScaleSensor(session, CONF_URL, "S", '0', None),
SpaceWeatherScaleSensor(session, CONF_URL, "G", '0', ''), SpaceWeatherScaleSensor(session, CONF_URL, "G", '0', None),
SpaceWeatherScaleSensor(session, CONF_URL, "R", '-1', '_24hr'), SpaceWeatherScaleSensor(session, CONF_URL, "R", '-1', '24hr_max'),
SpaceWeatherScaleSensor(session, CONF_URL, "S", '-1', '_24hr'), SpaceWeatherScaleSensor(session, CONF_URL, "S", '-1', '24hr_max'),
SpaceWeatherScaleSensor(session, CONF_URL, "G", '-1', '_24hr'), SpaceWeatherScaleSensor(session, CONF_URL, "G", '-1', '24hr_max'),
SpaceWeatherPredictionSensor(session, CONF_URL, "R", "MinorProb", "pred_r_minor"),
SpaceWeatherPredictionSensor(session, CONF_URL, "R", "MajorProb", "pred_r_major"), SpaceWeatherPredictionSensor(session, CONF_URL, "R", "MinorProb", "1", 'today'),
SpaceWeatherPredictionSensor(session, CONF_URL, "S", "Scale", "pred_s_scale"), SpaceWeatherPredictionSensor(session, CONF_URL, "R", "MajorProb", "1", 'today'),
SpaceWeatherPredictionSensor(session, CONF_URL, "S", "Prob", "pred_s_prob"), SpaceWeatherPredictionSensor(session, CONF_URL, "S", "Scale", "1", 'today'),
SpaceWeatherPredictionSensor(session, CONF_URL, "G", "Scale", "pred_g_scale"), SpaceWeatherPredictionSensor(session, CONF_URL, "S", "Prob", "1", 'today'),
SpaceWeatherDateStampSensor(session, CONF_URL), SpaceWeatherPredictionSensor(session, CONF_URL, "G", "Scale", "1", 'today'),
SpaceWeatherPredictionSensor(session, CONF_URL, "R", "MinorProb", "2", '1day'),
SpaceWeatherPredictionSensor(session, CONF_URL, "R", "MajorProb", "2", '1day'),
SpaceWeatherPredictionSensor(session, CONF_URL, "S", "Scale", "2", '1day'),
SpaceWeatherPredictionSensor(session, CONF_URL, "S", "Prob", "2", '1day'),
SpaceWeatherPredictionSensor(session, CONF_URL, "G", "Scale", "2", '1day'),
SpaceWeatherPredictionSensor(session, CONF_URL, "R", "MinorProb", "3", '2day'),
SpaceWeatherPredictionSensor(session, CONF_URL, "R", "MajorProb", "3", '2day'),
SpaceWeatherPredictionSensor(session, CONF_URL, "S", "Scale", "3", '2day'),
SpaceWeatherPredictionSensor(session, CONF_URL, "S", "Prob", "3", '2day'),
SpaceWeatherPredictionSensor(session, CONF_URL, "G", "Scale", "3", '2day'),
], True) ], True)
@ -35,9 +45,13 @@ class SpaceWeatherScaleSensor(Entity):
self._session = session self._session = session
self._url = url self._url = url
self._scale_key = scale_key self._scale_key = scale_key
self._name = f'Space Weather Scale {scale_key} {trailing}'
self._state = None self._name = f'Space Weather Scale {scale_key}'
if trailing is not None and len(trailing):
self._name = self._name + ' ' + trailing.replace("_", " ").replace(" ", " ")
self._data = None self._data = None
assert isinstance(data_selector, str)
self._data_selector = data_selector self._data_selector = data_selector
self._trailing = trailing self._trailing = trailing
@ -47,18 +61,22 @@ class SpaceWeatherScaleSensor(Entity):
@property @property
def unique_id(self): def unique_id(self):
return f"space_weather_scale_{self._scale_key.lower()}{self._trailing.replace('_', '')}" s = f"space_weather_scale_{self._scale_key.lower()}"
if self._trailing is not None and len(self._trailing):
s = s + '_' + self._trailing.strip('_')
return s
@property @property
def state(self): def state(self):
return self._state return f'{self._scale_key}{self._data[self._scale_key]["Scale"]}'
@property @property
def device_state_attributes(self): def extra_state_attributes(self):
if self._data: if self._data:
return { return {
"scale": self._data[self._scale_key]["Scale"], "scale_int": int(self._data[self._scale_key]["Scale"]),
"text": self._data[self._scale_key]["Text"], "text": self._data[self._scale_key]["Text"],
"timestamp": datetime.fromisoformat(self._data["DateStamp"] + 'T' + self._data["TimeStamp"] + '+00:00').isoformat()
} }
return None return None
@ -69,7 +87,6 @@ class SpaceWeatherScaleSensor(Entity):
if response.status == 200: if response.status == 200:
data = await response.json() data = await response.json()
self._data = data[self._data_selector] self._data = data[self._data_selector]
self._state = f'{self._scale_key}{self._data[self._scale_key]["Scale"]}'
else: else:
_LOGGER.error(f"Error fetching data from {self._url}") _LOGGER.error(f"Error fetching data from {self._url}")
except aiohttp.ClientError as err: except aiohttp.ClientError as err:
@ -77,13 +94,14 @@ class SpaceWeatherScaleSensor(Entity):
class SpaceWeatherPredictionSensor(Entity): class SpaceWeatherPredictionSensor(Entity):
def __init__(self, session, url, scale_key, pred_key, unique_id): def __init__(self, session, url, scale_key, pred_key, data_selector, trailing):
self._session = session self._session = session
self._url = url self._url = url
self._scale_key = scale_key self._scale_key = scale_key
self._pred_key = pred_key self._pred_key = pred_key
self._unique_id = unique_id self._data_selector = data_selector
self._name = f'Space Weather Prediction {scale_key} {pred_key}' self._trailing = trailing
self._name = f'Space Weather Prediction {scale_key} {pred_key} {trailing.replace("_", " ").replace(" ", " ")}'
self._state = None self._state = None
self._data = None self._data = None
@ -93,7 +111,7 @@ class SpaceWeatherPredictionSensor(Entity):
@property @property
def unique_id(self): def unique_id(self):
return self._unique_id return f'space_weather_pred_{self._scale_key}_{self._pred_key}_{self._trailing}'.lower()
@property @property
def state(self): def state(self):
@ -113,11 +131,10 @@ class SpaceWeatherPredictionSensor(Entity):
return None return None
@property @property
def device_state_attributes(self): def extra_state_attributes(self):
if self._data: if self._data:
return { return {
"date_stamp": self._data["DateStamp"], "timestamp": datetime.fromisoformat(self._data["DateStamp"] + 'T' + self._data["TimeStamp"] + '+00:00').isoformat()
"time_stamp": self._data["TimeStamp"],
} }
return None return None
@ -127,64 +144,9 @@ class SpaceWeatherPredictionSensor(Entity):
async with self._session.get(self._url) as response: async with self._session.get(self._url) as response:
if response.status == 200: if response.status == 200:
data = await response.json() data = await response.json()
now = datetime.now() + timedelta(days=1) self._data = data[self._data_selector]
tomorrow_date = now.strftime('%Y-%m-%d')
tomorrow_data = {}
for k, v in data.items():
datestamp = v['DateStamp']
if datestamp == tomorrow_date:
tomorrow_data = v
assert len(tomorrow_data.keys()) is not None
self._data = tomorrow_data
self._state = self._data[self._scale_key][self._pred_key] self._state = self._data[self._scale_key][self._pred_key]
else: else:
_LOGGER.error(f"Error fetching data from {self._url}") _LOGGER.error(f"Error fetching data from {self._url}")
except aiohttp.ClientError as err: except aiohttp.ClientError as err:
_LOGGER.error(f"Error fetching data from {self._url}: {err}") _LOGGER.error(f"Error fetching data from {self._url}: {err}")
class SpaceWeatherDateStampSensor(Entity):
"""
Attributes don't seem to be working so we use a single sensor to track the timestamp of the space weather
prediction updated.
"""
def __init__(self, session, url):
self._session = session
self._url = url
self._name = "Space Weather Prediction Date Stamp"
self._state = None
self._data = None
@property
def name(self):
return self._name
@property
def unique_id(self):
return "space_weather_prediction_date_stamp"
@property
def state(self):
return self._state
@Throttle(SCAN_INTERVAL)
async def async_update(self):
try:
async with self._session.get(self._url) as response:
if response.status == 200:
data = await response.json()
now = datetime.now() + timedelta(days=1)
tomorrow_date = now.strftime('%Y-%m-%d')
tomorrow_data = {}
for k, v in data.items():
datestamp = v['DateStamp']
if datestamp == tomorrow_date:
tomorrow_data = v
assert len(tomorrow_data.keys()) is not None
self._data = tomorrow_data
self._state = datetime.strptime(f'{self._data["DateStamp"]}', "%Y-%m-%d").strftime('%m-%d-%Y')
else:
_LOGGER.error(f"Error fetching data from {self._url}")
except aiohttp.ClientError as err:
_LOGGER.error(f"Error fetching data from {self._url}: {err}")

View File

@ -11,7 +11,7 @@
To add these custom cards, create a card of the "Manual" type. To add these custom cards, create a card of the "Manual" type.
``` ```
type: space-weather-card-current type: space-weather-current
type: space-weather-prediction-card-1day type: space-weather-prediction-1day
type: space-weather-card-24hr-max type: space-weather-24hr-max
``` ```

View File

@ -75,7 +75,7 @@ class SpaceWeather24hrMaxCard extends HTMLElement {
margin-bottom: 16px; margin-bottom: 16px;
text-align: center; text-align: center;
} }
.scale-item { .scale-item {
cursor: pointer; cursor: pointer;
} }
@ -87,28 +87,28 @@ class SpaceWeather24hrMaxCard extends HTMLElement {
</div> </div>
<div class="card-content"> <div class="card-content">
<div class="scale-container"> <div class="scale-container">
<div class="scale-item" data-entity-id="sensor.space_weather_scale_r_24hr"> <div class="scale-item" data-entity-id="sensor.space_weather_scale_r_24hr_max">
<div class="scale-value noaa_scale_bg_${this._getNumericState('sensor.space_weather_scale_r_24hr')}"> <div class="scale-value noaa_scale_bg_${this._getNumericState('sensor.space_weather_scale_r_24hr_max')}">
${this._getStateValue('sensor.space_weather_scale_r_24hr')} ${this._getStateValue('sensor.space_weather_scale_r_24hr_max')}
</div> </div>
<div class="scale-text"> <div class="scale-text">
${this._getStateAttribute('sensor.space_weather_scale_r_24hr', 'text')} ${this._getStateAttribute('sensor.space_weather_scale_r_24hr_max', 'text')}
</div> </div>
</div> </div>
<div class="scale-item" data-entity-id="sensor.space_weather_scale_s_24hr"> <div class="scale-item" data-entity-id="sensor.space_weather_scale_s_24hr_max">
<div class="scale-value noaa_scale_bg_${this._getNumericState('sensor.space_weather_scale_s_24hr')}"> <div class="scale-value noaa_scale_bg_${this._getNumericState('sensor.space_weather_scale_s_24hr_max')}">
${this._getStateValue('sensor.space_weather_scale_s_24hr')} ${this._getStateValue('sensor.space_weather_scale_s_24hr_max')}
</div> </div>
<div class="scale-text"> <div class="scale-text">
${this._getStateAttribute('sensor.space_weather_scale_s_24hr', 'text')} ${this._getStateAttribute('sensor.space_weather_scale_s_24hr_max', 'text')}
</div> </div>
</div> </div>
<div class="scale-item" data-entity-id="sensor.space_weather_scale_g_24hr"> <div class="scale-item" data-entity-id="sensor.space_weather_scale_g_24hr_max">
<div class="scale-value noaa_scale_bg_${this._getNumericState('sensor.space_weather_scale_g_24hr')}"> <div class="scale-value noaa_scale_bg_${this._getNumericState('sensor.space_weather_scale_g_24hr_max')}">
${this._getStateValue('sensor.space_weather_scale_g_24hr')} ${this._getStateValue('sensor.space_weather_scale_g_24hr_max')}
</div> </div>
<div class="scale-text"> <div class="scale-text">
${this._getStateAttribute('sensor.space_weather_scale_g_24hr', 'text')} ${this._getStateAttribute('sensor.space_weather_scale_g_24hr_max', 'text')}
</div> </div>
</div> </div>
</div> </div>
@ -154,4 +154,4 @@ class SpaceWeather24hrMaxCard extends HTMLElement {
} }
} }
customElements.define('space-weather-card-24hr-max', SpaceWeather24hrMaxCard); customElements.define('space-weather-24hr-max', SpaceWeather24hrMaxCard);

View File

@ -75,7 +75,7 @@ class SpaceWeatherCard extends HTMLElement {
margin-bottom: 16px; margin-bottom: 16px;
text-align: center; text-align: center;
} }
.scale-item { .scale-item {
cursor: pointer; cursor: pointer;
} }
@ -157,4 +157,4 @@ class SpaceWeatherCard extends HTMLElement {
} }
} }
customElements.define('space-weather-card-current', SpaceWeatherCard); customElements.define('space-weather-current', SpaceWeatherCard);

View File

@ -2,6 +2,7 @@ class SpaceWeatherPredictionCard extends HTMLElement {
constructor() { constructor() {
super(); super();
this.attachShadow({mode: 'open'}); this.attachShadow({mode: 'open'});
this._selectedDay = 'today';
} }
setConfig(config) { setConfig(config) {
@ -19,7 +20,7 @@ class SpaceWeatherPredictionCard extends HTMLElement {
this.shadowRoot.innerHTML = ` this.shadowRoot.innerHTML = `
<style> <style>
/* TODO: unify this with the other card */ /* TODO: unify this with the other card */
.prediction-container { .prediction-container {
display: grid; display: grid;
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(4, 1fr);
@ -72,7 +73,7 @@ class SpaceWeatherPredictionCard extends HTMLElement {
padding: 0 16px 2px 16px; padding: 0 16px 2px 16px;
text-align: center; text-align: center;
} }
.card-subheader { .card-subheader {
padding: 0 16px 16px 16px; padding: 0 16px 16px 16px;
margin-bottom: 16px; margin-bottom: 16px;
@ -80,7 +81,7 @@ class SpaceWeatherPredictionCard extends HTMLElement {
font-style: italic; font-style: italic;
font-size: 15px; font-size: 15px;
} }
.scale-value { .scale-value {
font-size: 24px; font-size: 24px;
font-weight: bold; font-weight: bold;
@ -88,57 +89,82 @@ class SpaceWeatherPredictionCard extends HTMLElement {
padding: 8px; padding: 8px;
border-radius: 4px; border-radius: 4px;
} }
a { a {
text-decoration: none; text-decoration: none;
color: inherit; color: inherit;
} }
.button-container {
display: flex;
justify-content: center;
margin-top: 16px;
}
.day-button {
margin: 0 8px;
padding: 8px 16px;
border: none;
border-radius: 4px;
background-color: #e5e5e5;
cursor: pointer;
}
.day-button.selected {
background-color: #3788d8;
color: #fff;
}
</style> </style>
<ha-card> <ha-card>
<div class="card-header">Space Weather Predictions</div> <div class="card-header">Space Weather Predictions</div>
<div class="card-subheader"> <div class="card-subheader">
For ${this._getStateValue('sensor.space_weather_prediction_date_stamp')} For ${this._getAttribute(`sensor.space_weather_prediction_r_minorprob_${this._selectedDay}`, 'timestamp').split('T')[0]}
</div> </div>
<div class="card-content"> <div class="card-content">
<div class="prediction-container"> <div class="prediction-container">
<div class="prediction-item" data-entity-id="sensor.space_weather_prediction_r_minorprob"> <div class="prediction-item" data-entity-id="sensor.space_weather_prediction_r_minorprob_${this._selectedDay}">
<div class="prediction-label">R1-R2</div> <div class="prediction-label">R1-R2</div>
<!-- TODO: what happens when "Scale" in JSON is not null? --> <!-- TODO: what happens when "Scale" in JSON is not null? -->
<!-- TODO: what happens when "Text" in JSON is not null? --> <!-- TODO: what happens when "Text" in JSON is not null? -->
<div class="prediction-value"> <div class="prediction-value">
${Math.round(parseFloat(this._getStateValue('sensor.space_weather_prediction_r_minorprob')))}% ${Math.round(parseFloat(this._getStateValue(`sensor.space_weather_prediction_r_minorprob_${this._selectedDay}`)))}%
</div> </div>
</div> </div>
<div class="prediction-item" data-entity-id="sensor.space_weather_prediction_r_majorprob"> <div class="prediction-item" data-entity-id="sensor.space_weather_prediction_r_majorprob_${this._selectedDay}">
<div class="prediction-label">R3-R5</div> <div class="prediction-label">R3-R5</div>
<!-- TODO: what happens when "Scale" in JSON is not null? --> <!-- TODO: what happens when "Scale" in JSON is not null? -->
<!-- TODO: what happens when "Text" in JSON is not null? --> <!-- TODO: what happens when "Text" in JSON is not null? -->
<div class="prediction-value"> <div class="prediction-value">
${Math.round(parseFloat(this._getStateValue('sensor.space_weather_prediction_r_majorprob')))}% ${Math.round(parseFloat(this._getStateValue(`sensor.space_weather_prediction_r_majorprob_${this._selectedDay}`)))}%
</div> </div>
</div> </div>
<div class="prediction-item" data-entity-id="sensor.space_weather_prediction_s_prob"> <div class="prediction-item" data-entity-id="sensor.space_weather_prediction_s_prob_${this._selectedDay}">
<div class="prediction-label">S1 or Greater</div> <div class="prediction-label">S1 or Greater</div>
<!-- TODO: what happens when "Scale" in JSON is not null? --> <!-- TODO: what happens when "Scale" in JSON is not null? -->
<!-- TODO: what happens when "Text" in JSON is not null? --> <!-- TODO: what happens when "Text" in JSON is not null? -->
<div class="prediction-value"> <div class="prediction-value">
${Math.round(parseFloat(this._getStateValue('sensor.space_weather_prediction_s_prob')))}% ${Math.round(parseFloat(this._getStateValue(`sensor.space_weather_prediction_s_prob_${this._selectedDay}`)))}%
</div> </div>
</div> </div>
<!-- <div class="prediction-item"> <!-- <div class="prediction-item">
<div class="prediction-label">S Probability</div> <div class="prediction-label">S Probability</div>
<div class="prediction-value"> <div class="prediction-value">
${this._getStateValue('sensor.space_weather_prediction_s_scale')} ${this._getStateValue('sensor.space_weather_prediction_s_scale_today')}
</div> </div>
</div> --> </div> -->
<div class="prediction-item" data-entity-id="sensor.space_weather_prediction_g_scale"> <div class="prediction-item" data-entity-id="sensor.space_weather_prediction_g_scale_${this._selectedDay}">
<div class="prediction-label">G Scale</div> <div class="prediction-label">G Scale</div>
<div class="prediction-value scale-value noaa_scale_bg_${this._getStateValue('sensor.space_weather_prediction_g_scale')}"> <div class="prediction-value scale-value noaa_scale_bg_${this._getStateValue(`sensor.space_weather_prediction_g_scale_${this._selectedDay}`)}">
G${this._getStateValue('sensor.space_weather_prediction_g_scale')} G${this._getStateValue(`sensor.space_weather_prediction_g_scale_${this._selectedDay}`)}
</div> </div>
</div> </div>
</div> </div>
<div class="button-container">
<button class="day-button ${this._selectedDay === 'today' ? 'selected' : ''}" data-day="today">Today</button>
<button class="day-button ${this._selectedDay === '1day' ? 'selected' : ''}" data-day="1day">1 Day</button>
<button class="day-button ${this._selectedDay === '2day' ? 'selected' : ''}" data-day="2day">2 Day</button>
</div>
</div> </div>
</ha-card> </ha-card>
`; `;
@ -172,6 +198,15 @@ class SpaceWeatherPredictionCard extends HTMLElement {
this._handleClick(entityId); this._handleClick(entityId);
}); });
}); });
const dayButtons = this.shadowRoot.querySelectorAll('.day-button');
dayButtons.forEach(button => {
button.addEventListener('click', () => {
const day = button.dataset.day;
this._selectedDay = day;
this.render();
});
});
} }
_handleClick(entityId) { _handleClick(entityId) {
@ -181,4 +216,4 @@ class SpaceWeatherPredictionCard extends HTMLElement {
} }
} }
customElements.define('space-weather-prediction-card-1day', SpaceWeatherPredictionCard); customElements.define('space-weather-prediction', SpaceWeatherPredictionCard);

View File

@ -73,7 +73,7 @@ def main():
break break
latest = round(avg_tec, 1) latest = round(avg_tec, 1)
publish('vtec', latest) publish('vtec', latest)
time.sleep(1800) time.sleep(900)
if __name__ == '__main__': if __name__ == '__main__':