diff --git a/CHANGELOG.md b/CHANGELOG.md index 1904168..296b1aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,21 @@ All notable changes to NOAA It All for Home Assistant will be documented in this The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.7.0] - Current +## [0.7.1] - Current + +### Added +- **East Pacific tropical outlook image.** `image.noaa_hurricane_pacific_outlook_image`, on the + existing **NOAA Hurricane** device. The NHC publishes its two-day tropical weather outlook as + one graphic per basin, so the Atlantic map that shipped until now showed nothing at all for a + storm off Baja California. Both basins now have their own tile. + +### Changed +- The Atlantic outlook image (`image.noaa_hurricane_outlook_image`) now uses NHC's larger + `xgtwo_atl_2d0.png` rendering instead of `two_atl_2d0.png`. Same map, more readable on a + dashboard. The entity ID and name are unchanged, so existing cards and automations keep + working. + +## [0.7.0] ### Added - **Solar and lunar eclipses, worked out for where you actually are.** Three sensors and two diff --git a/CONFIGURATION.md b/CONFIGURATION.md index 27a0042..fa1be08 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -132,14 +132,15 @@ office code in lower case, leaving the exceptions below untouched. ### Exceptions to the pattern -These five entity IDs contain **no office code** — they live on the shared, office-independent +These six entity IDs contain **no office code** — they live on the shared, office-independent `NOAA Hurricane` device. Do not substitute into them: | Entity ID | Why | |---|---| | `sensor.noaa_hurricane_alerts` | Global NHC data on the shared `NOAA Hurricane` device | | `sensor.noaa_hurricane_activity` | Global NHC data on the shared `NOAA Hurricane` device | -| `image.noaa_hurricane_outlook_image` | Global NHC imagery | +| `image.noaa_hurricane_outlook_image` | Global NHC imagery (Atlantic) | +| `image.noaa_hurricane_pacific_outlook_image` | Global NHC imagery (East Pacific) | | `image.noaa_hurricane_goes_air_mass` | Global GOES imagery | | `image.noaa_hurricane_goes_geocolor` | Global GOES imagery | @@ -250,6 +251,7 @@ regardless of how many offices you configure, and carry **no office code**. - `sensor.noaa_hurricane_alerts` - `sensor.noaa_hurricane_activity` - `image.noaa_hurricane_outlook_image` +- `image.noaa_hurricane_pacific_outlook_image` - `image.noaa_hurricane_goes_air_mass` - `image.noaa_hurricane_goes_geocolor` diff --git a/README.md b/README.md index f1b3cd1..1485600 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ Global hurricane tracking and GOES satellite imagery — **created once**, share - **Device ID**: `noaa_weather_hurricane` - **Location**: Independent (global NHC/GOES data) - **Update Frequency**: 5 minutes -- **Entities**: `sensor.noaa_hurricane_activity`, `sensor.noaa_hurricane_alerts`, `image.noaa_hurricane_outlook_image`, `image.noaa_hurricane_goes_air_mass`, `image.noaa_hurricane_goes_geocolor` +- **Entities**: `sensor.noaa_hurricane_activity`, `sensor.noaa_hurricane_alerts`, `image.noaa_hurricane_outlook_image`, `image.noaa_hurricane_pacific_outlook_image`, `image.noaa_hurricane_goes_air_mass`, `image.noaa_hurricane_goes_geocolor`
@@ -287,7 +287,8 @@ Visual representations of current conditions:
- **Aurora Forecast Image** — Tonight's aurora coverage forecast *(image.noaa_ilm_space_aurora_forecast_image)*
**NOAA Hurricane** (global, created once):
-- **Outlook Image** — 2-day tropical weather outlook from NHC *(image.noaa_hurricane_outlook_image)*
+- **Outlook Image** — 2-day tropical weather outlook from NHC, **Atlantic basin** *(image.noaa_hurricane_outlook_image)*
+- **Pacific Outlook Image** — the same 2-day outlook for the **East Pacific basin** *(image.noaa_hurricane_pacific_outlook_image)*
- **GOES Air Mass** — GOES-19 Air Mass RGB satellite imagery *(image.noaa_hurricane_goes_air_mass)*
- **GOES Geocolor** — GOES-19 GeoColor satellite imagery *(image.noaa_hurricane_goes_geocolor)*
@@ -351,13 +352,14 @@ replace `ilm` with your office code in lower case — and leave the entities lis
- `sensor.noaa_ilm_weather_temperature` — Temperature for Wilmington (ILM office)
- `binary_sensor.noaa_ilm_surf_unsafe_to_swim` — Rip current safety for Wilmington
- `sensor.noaa_hurricane_activity` — Global hurricane activity (NOAA Hurricane device)
-- `image.noaa_hurricane_outlook_image` — Hurricane outlook image (NOAA Hurricane device)
+- `image.noaa_hurricane_outlook_image` — Atlantic hurricane outlook image (NOAA Hurricane device)
+- `image.noaa_hurricane_pacific_outlook_image` — East Pacific hurricane outlook image (NOAA Hurricane device)
- `image.noaa_ilm_weather_radar_base_reflectivity` — Radar base reflectivity for Wilmington (ILM)
- `image.noaa_ilm_weather_radar_loop` — Radar loop for Wilmington (ILM)
### Exceptions to the pattern
-These five entity IDs do **not** contain an office code, because they live on the shared,
+These six entity IDs do **not** contain an office code, because they live on the shared,
office-independent `NOAA Hurricane` device. When you copy an example and swap the office code,
skip these — substituting into them produces an entity that does not exist:
@@ -366,6 +368,7 @@ skip these — substituting into them produces an entity that does not exist:
| `sensor.noaa_hurricane_alerts` | Global NHC data, on the shared `NOAA Hurricane` device |
| `sensor.noaa_hurricane_activity` | Global NHC data, on the shared `NOAA Hurricane` device |
| `image.noaa_hurricane_outlook_image` | Global NHC data, on the shared `NOAA Hurricane` device |
+| `image.noaa_hurricane_pacific_outlook_image` | Global NHC data, on the shared `NOAA Hurricane` device |
| `image.noaa_hurricane_goes_air_mass` | Global GOES imagery, on the shared `NOAA Hurricane` device |
| `image.noaa_hurricane_goes_geocolor` | Global GOES imagery, on the shared `NOAA Hurricane` device |
diff --git a/custom_components/noaa_it_all/image.py b/custom_components/noaa_it_all/image.py
index 3757041..687f650 100644
--- a/custom_components/noaa_it_all/image.py
+++ b/custom_components/noaa_it_all/image.py
@@ -52,7 +52,13 @@
'north/latest.jpg')
# NOAA Hurricane Image Sources
-HURRICANE_OUTLOOK_URL = 'https://www.nhc.noaa.gov/xgtwo/two_atl_2d0.png'
+# The National Hurricane Center publishes its two-day tropical weather
+# outlook as one graphic per basin, so the Atlantic map alone leaves East
+# Pacific systems invisible -- both basins get their own entity. The
+# ``xgtwo_`` prefix is NHC's larger rendering of the same map as the older
+# ``two_`` graphic.
+HURRICANE_OUTLOOK_URL = 'https://www.nhc.noaa.gov/xgtwo/xgtwo_atl_2d0.png'
+HURRICANE_PACIFIC_OUTLOOK_URL = 'https://www.nhc.noaa.gov/xgtwo/xgtwo_pac_2d0.png'
# NOAA GOES Satellite Image Sources
GOES_AIRMASS_URL = 'https://cdn.star.nesdis.noaa.gov/GOES19/ABI/CONUS/AirMass/1250x750.jpg'
@@ -193,6 +199,7 @@ async def async_setup_entry(
if not domain_data.get(HURRICANE_IMAGES_ADDED_KEY):
entities.extend([
HurricaneOutlookImageEntity(hass),
+ HurricanePacificOutlookImageEntity(hass),
GOESAirMassImageEntity(hass),
GOESGeoColorImageEntity(hass),
])
@@ -649,12 +656,17 @@ def device_info(self) -> DeviceInfo:
class HurricaneOutlookImageEntity(NoaaImageEntity):
- """Representation of the Hurricane Outlook Image.
+ """Representation of the Atlantic Hurricane Outlook Image.
+
+ Covers the Atlantic basin only; East Pacific systems are on the
+ companion :class:`HurricanePacificOutlookImageEntity` map.
Uses ``_attr_has_entity_name = True`` so that Home Assistant
automatically combines the device name ("NOAA Hurricane") with the
local entity name ("Outlook Image") to produce the entity ID
- ``image.noaa_hurricane_outlook_image``.
+ ``image.noaa_hurricane_outlook_image``. The name is deliberately
+ left basin-neutral so that existing dashboards and automations keep
+ working.
"""
_attr_has_entity_name = True
@@ -686,6 +698,48 @@ def device_info(self) -> DeviceInfo:
return _hurricane_device_info()
+class HurricanePacificOutlookImageEntity(NoaaImageEntity):
+ """Representation of the East Pacific Hurricane Outlook Image.
+
+ The NHC two-day outlook is published per basin, so the Atlantic map
+ never shows an East Pacific system. This is the same graphic for the
+ Pacific basin.
+
+ Uses ``_attr_has_entity_name = True`` so that Home Assistant
+ automatically combines the device name ("NOAA Hurricane") with the
+ local entity name ("Pacific Outlook Image") to produce the entity ID
+ ``image.noaa_hurricane_pacific_outlook_image``.
+ """
+
+ _attr_has_entity_name = True
+ _attr_content_type = "image/png"
+ _log_label = "Pacific hurricane outlook"
+ _url = HURRICANE_PACIFIC_OUTLOOK_URL
+
+ def __init__(self, hass, office_code=None):
+ """Initialize the image entity.
+
+ ``office_code`` is accepted for backward compatibility but is
+ unused: this entity is global (NHC).
+ """
+ super().__init__(hass)
+
+ @property
+ def name(self):
+ """Return the local entity name."""
+ return 'Pacific Outlook Image'
+
+ @property
+ def unique_id(self):
+ """Return a unique ID for this entity."""
+ return 'noaa_hurricane_pacific_outlook_image'
+
+ @property
+ def device_info(self) -> DeviceInfo:
+ """Return device information."""
+ return _hurricane_device_info()
+
+
class RadarBaseReflectivityImageEntity(NoaaImageEntity):
"""Representation of the Radar Base Reflectivity Image for a specific location.
diff --git a/custom_components/noaa_it_all/manifest.json b/custom_components/noaa_it_all/manifest.json
index 6f5ea43..2658623 100644
--- a/custom_components/noaa_it_all/manifest.json
+++ b/custom_components/noaa_it_all/manifest.json
@@ -13,5 +13,5 @@
"aiohttp",
"voluptuous"
],
- "version": "0.7.0"
+ "version": "0.7.1"
}
diff --git a/tests/test_image.py b/tests/test_image.py
index 2c201a5..5611a74 100644
--- a/tests/test_image.py
+++ b/tests/test_image.py
@@ -271,6 +271,57 @@ def test_device_info_uses_hurricane_device(self):
self.assertIn((DOMAIN, HURRICANE_DEVICE_ID), info["identifiers"])
+class TestHurricanePacificOutlookImageEntity(unittest.TestCase):
+ """Tests for HurricanePacificOutlookImageEntity properties."""
+
+ def _make(self):
+ from noaa_it_all.image import HurricanePacificOutlookImageEntity
+ return HurricanePacificOutlookImageEntity(HASS)
+
+ def test_name(self):
+ # Local name only; HA prepends "NOAA Hurricane" to form the full name.
+ entity = self._make()
+ self.assertEqual(entity.name, "Pacific Outlook Image")
+
+ def test_unique_id(self):
+ entity = self._make()
+ self.assertEqual(entity.unique_id, "noaa_hurricane_pacific_outlook_image")
+
+ def test_has_entity_name(self):
+ from noaa_it_all.image import HurricanePacificOutlookImageEntity
+ self.assertTrue(HurricanePacificOutlookImageEntity._attr_has_entity_name)
+
+ def test_device_info_uses_hurricane_device(self):
+ from noaa_it_all.const import DOMAIN, HURRICANE_DEVICE_ID
+ entity = self._make()
+ info = entity.device_info
+ self.assertIn((DOMAIN, HURRICANE_DEVICE_ID), info["identifiers"])
+
+
+class TestHurricaneOutlookSources(unittest.TestCase):
+ """The two outlook entities must point at different NHC basins.
+
+ A copy-paste slip that leaves both on the Atlantic graphic would show two
+ identical maps on the dashboard, which is exactly the bug this pair of
+ entities exists to fix, and nothing else in the suite would catch it.
+ """
+
+ def test_each_basin_has_its_own_graphic(self):
+ from noaa_it_all.image import (
+ HurricaneOutlookImageEntity,
+ HurricanePacificOutlookImageEntity,
+ )
+ atlantic = HurricaneOutlookImageEntity(HASS)._url
+ pacific = HurricanePacificOutlookImageEntity(HASS)._url
+ self.assertEqual(
+ atlantic, "https://www.nhc.noaa.gov/xgtwo/xgtwo_atl_2d0.png"
+ )
+ self.assertEqual(
+ pacific, "https://www.nhc.noaa.gov/xgtwo/xgtwo_pac_2d0.png"
+ )
+ self.assertNotEqual(atlantic, pacific)
+
+
class TestRadarBaseReflectivityImageEntity(unittest.TestCase):
"""Tests for RadarBaseReflectivityImageEntity properties."""
@@ -405,11 +456,13 @@ class TestTwoOfficeSetup(unittest.TestCase):
def _make_hurricane_entities(self):
from noaa_it_all.image import (
HurricaneOutlookImageEntity,
+ HurricanePacificOutlookImageEntity,
GOESAirMassImageEntity,
GOESGeoColorImageEntity,
)
return [
HurricaneOutlookImageEntity(HASS),
+ HurricanePacificOutlookImageEntity(HASS),
GOESAirMassImageEntity(HASS),
GOESGeoColorImageEntity(HASS),
]
@@ -442,8 +495,11 @@ def test_hurricane_entity_ids_are_stable(self):
"""Hurricane unique_ids are constant — not per-office."""
entities = self._make_hurricane_entities()
self.assertEqual(entities[0].unique_id, "noaa_hurricane_outlook_image")
- self.assertEqual(entities[1].unique_id, "noaa_hurricane_goes_air_mass")
- self.assertEqual(entities[2].unique_id, "noaa_hurricane_goes_geocolor")
+ self.assertEqual(
+ entities[1].unique_id, "noaa_hurricane_pacific_outlook_image"
+ )
+ self.assertEqual(entities[2].unique_id, "noaa_hurricane_goes_air_mass")
+ self.assertEqual(entities[3].unique_id, "noaa_hurricane_goes_geocolor")
# ------------------------------------------------------------------
# Radar entities are per-office
@@ -858,6 +914,7 @@ def _all_entities(self):
GOESGeoColorImageEntity,
GeoelectricFieldImageEntity,
HurricaneOutlookImageEntity,
+ HurricanePacificOutlookImageEntity,
RadarBaseReflectivityImageEntity,
RadarLoopImageEntity,
)
@@ -865,6 +922,7 @@ def _all_entities(self):
GeoelectricFieldImageEntity(HASS, OFFICE),
AuroraForecastImageEntity(HASS, OFFICE),
HurricaneOutlookImageEntity(HASS),
+ HurricanePacificOutlookImageEntity(HASS),
RadarBaseReflectivityImageEntity(HASS, OFFICE, "KNKX"),
RadarLoopImageEntity(HASS, OFFICE, "KNKX"),
GOESAirMassImageEntity(HASS),
@@ -931,7 +989,7 @@ def test_the_upstream_url_is_refreshed_on_every_fetch(self):
class TestContentTypes(unittest.TestCase):
- """Home Assistant defaults every image to JPEG; five of seven are not."""
+ """Home Assistant defaults every image to JPEG; six of eight are not."""
def test_declared_content_types_match_the_upstream_formats(self):
from noaa_it_all.image import (
@@ -940,6 +998,7 @@ def test_declared_content_types_match_the_upstream_formats(self):
GOESGeoColorImageEntity,
GeoelectricFieldImageEntity,
HurricaneOutlookImageEntity,
+ HurricanePacificOutlookImageEntity,
RadarBaseReflectivityImageEntity,
RadarLoopImageEntity,
)
@@ -947,6 +1006,7 @@ def test_declared_content_types_match_the_upstream_formats(self):
(GeoelectricFieldImageEntity(HASS, OFFICE), "image/png"),
(AuroraForecastImageEntity(HASS, OFFICE), "image/jpeg"),
(HurricaneOutlookImageEntity(HASS), "image/png"),
+ (HurricanePacificOutlookImageEntity(HASS), "image/png"),
(RadarBaseReflectivityImageEntity(HASS, OFFICE, "KNKX"), "image/gif"),
(RadarLoopImageEntity(HASS, OFFICE, "KNKX"), "image/gif"),
(GOESAirMassImageEntity(HASS), "image/jpeg"),
@@ -963,6 +1023,7 @@ def test_log_labels_are_distinct(self):
GOESGeoColorImageEntity,
GeoelectricFieldImageEntity,
HurricaneOutlookImageEntity,
+ HurricanePacificOutlookImageEntity,
RadarBaseReflectivityImageEntity,
RadarLoopImageEntity,
)
@@ -970,6 +1031,7 @@ def test_log_labels_are_distinct(self):
GeoelectricFieldImageEntity(HASS, OFFICE)._log_label,
AuroraForecastImageEntity(HASS, OFFICE)._log_label,
HurricaneOutlookImageEntity(HASS)._log_label,
+ HurricanePacificOutlookImageEntity(HASS)._log_label,
RadarBaseReflectivityImageEntity(HASS, OFFICE, "KNKX")._log_label,
RadarLoopImageEntity(HASS, OFFICE, "KNKX")._log_label,
GOESAirMassImageEntity(HASS)._log_label,