Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand Down Expand Up @@ -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`

Expand Down
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

<p align="left">
<img width="327" height="380" alt="image" src="https://github.com/user-attachments/assets/7b484039-abe9-4c14-ab37-91b41d084411" />
Expand Down Expand Up @@ -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)*

Expand Down Expand Up @@ -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:

Expand All @@ -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 |

Expand Down
60 changes: 57 additions & 3 deletions custom_components/noaa_it_all/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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),
])
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion custom_components/noaa_it_all/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@
"aiohttp",
"voluptuous"
],
"version": "0.7.0"
"version": "0.7.1"
}
68 changes: 65 additions & 3 deletions tests/test_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down Expand Up @@ -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),
]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -858,13 +914,15 @@ def _all_entities(self):
GOESGeoColorImageEntity,
GeoelectricFieldImageEntity,
HurricaneOutlookImageEntity,
HurricanePacificOutlookImageEntity,
RadarBaseReflectivityImageEntity,
RadarLoopImageEntity,
)
return [
GeoelectricFieldImageEntity(HASS, OFFICE),
AuroraForecastImageEntity(HASS, OFFICE),
HurricaneOutlookImageEntity(HASS),
HurricanePacificOutlookImageEntity(HASS),
RadarBaseReflectivityImageEntity(HASS, OFFICE, "KNKX"),
RadarLoopImageEntity(HASS, OFFICE, "KNKX"),
GOESAirMassImageEntity(HASS),
Expand Down Expand Up @@ -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 (
Expand All @@ -940,13 +998,15 @@ def test_declared_content_types_match_the_upstream_formats(self):
GOESGeoColorImageEntity,
GeoelectricFieldImageEntity,
HurricaneOutlookImageEntity,
HurricanePacificOutlookImageEntity,
RadarBaseReflectivityImageEntity,
RadarLoopImageEntity,
)
expected = [
(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"),
Expand All @@ -963,13 +1023,15 @@ def test_log_labels_are_distinct(self):
GOESGeoColorImageEntity,
GeoelectricFieldImageEntity,
HurricaneOutlookImageEntity,
HurricanePacificOutlookImageEntity,
RadarBaseReflectivityImageEntity,
RadarLoopImageEntity,
)
labels = [
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,
Expand Down
Loading