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`

image @@ -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,