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
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ jobs:
- name: Install linting tools
run: |
python -m pip install --upgrade pip
pip install flake8 black isort mypy pytest
pip install flake8 black isort mypy pytest bandit types-aiofiles

- name: Run Black (code formatting check)
id: black
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ repos:
rev: 7.3.0
hooks:
- id: flake8
args: ["--max-line-length=88", "--extend-ignore=E203,W503,E501"]
args: ["--max-line-length=88", "--extend-ignore=E203,W503"]
files: ^(custom_components|tests)/.*\.py$

# Type checking
Expand Down
6 changes: 2 additions & 4 deletions custom_components/satcom_forecast/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,15 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
new = {**config_entry.data}
if "polling_interval" not in new:
new["polling_interval"] = 5 # Default to 5 minutes
hass.config_entries.async_update_entry(config_entry, data=new)
config_entry.version = 3
hass.config_entries.async_update_entry(config_entry, data=new, version=3)
Comment thread
clayauld marked this conversation as resolved.
_LOGGER.debug("Migration to version 3 successful")

if config_entry.version == 3:
# Version 3 to 4 migration - add default IMAP security
new = {**config_entry.data}
if "imap_security" not in new:
new["imap_security"] = "SSL" # Default to SSL for security
hass.config_entries.async_update_entry(config_entry, data=new)
config_entry.version = 4
hass.config_entries.async_update_entry(config_entry, data=new, version=4)
Comment thread
clayauld marked this conversation as resolved.
_LOGGER.debug("Migration to version 4 successful")

return True
Expand Down
3 changes: 2 additions & 1 deletion tests/test_api_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ async def test_cache_eviction(cache):

assert len(cache._cache) == 5

# endpoint_1 should be gone (it was inserted second, but endpoint_0 was accessed recently)
# endpoint_1 should be gone (it was inserted second, but endpoint_0 was accessed
# recently)
# Insertion order: 0, 1, 2, 3, 4.
# Access: 0.
# LRU order: 1, 2, 3, 4, 0.
Expand Down
14 changes: 10 additions & 4 deletions tests/test_api_compatibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,9 @@ async def test_weather_event_detection_compatibility(self):
start_time="2024-01-01T06:00:00-05:00",
end_time="2024-01-01T18:00:00-05:00",
is_daytime=True,
detailed_forecast="Rain likely with a high near 70. Chance of precipitation is 80%.",
detailed_forecast=(
"Rain likely with a high near 70. Chance of precipitation is 80%."
),
),
ForecastPeriod(
name="Tonight",
Expand All @@ -206,7 +208,9 @@ async def test_weather_event_detection_compatibility(self):
start_time="2024-01-02T06:00:00-05:00",
end_time="2024-01-02T18:00:00-05:00",
is_daytime=True,
detailed_forecast="Snow likely with a high near 30. Heavy snow possible.",
detailed_forecast=(
"Snow likely with a high near 30. Heavy snow possible."
),
),
]

Expand Down Expand Up @@ -321,7 +325,8 @@ async def test_character_count_targets(self, test_coordinates):
assert len(full) >= 500, f"Full length {len(full)} below minimum 500"

print(
f"✓ {lat}, {lon}: Summary={len(summary)}, Compact={len(compact)}, Full={len(full)}"
f"✓ {lat}, {lon}: Summary={len(summary)}, "
f"Compact={len(compact)}, Full={len(full)}"
)

except Exception as e:
Expand Down Expand Up @@ -365,7 +370,8 @@ async def test_days_parameter_compatibility(self):
assert len(api_forecast) > 50

print(
f"Days {days}: HTML={len(html_forecast)} chars, API={len(api_forecast)} chars"
f"Days {days}: HTML={len(html_forecast)} chars, "
f"API={len(api_forecast)} chars"
)

except Exception as e:
Expand Down
12 changes: 9 additions & 3 deletions tests/test_api_data_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ def sample_api_response(self):
"windSpeed": "10 to 15 mph",
"windDirection": "NW",
"shortForecast": "Sunny",
"detailedForecast": "Sunny with a high near 75. Northwest wind 10 to 15 mph.",
"detailedForecast": (
"Sunny with a high near 75. Northwest wind 10 to 15 mph."
),
"probabilityOfPrecipitation": {"value": 0},
"relativeHumidity": {"value": 45},
"heatIndex": None,
Expand All @@ -54,7 +56,9 @@ def sample_api_response(self):
"windSpeed": "5 to 10 mph",
"windDirection": "NW",
"shortForecast": "Clear",
"detailedForecast": "Clear with a low around 55. Northwest wind 5 to 10 mph.",
"detailedForecast": (
"Clear with a low around 55. Northwest wind 5 to 10 mph."
),
"probabilityOfPrecipitation": {"value": 0},
"relativeHumidity": {"value": 60},
"heatIndex": None,
Expand Down Expand Up @@ -100,7 +104,9 @@ def test_extract_weather_events_rain(self, processor):
start_time="2024-01-01T06:00:00-05:00",
end_time="2024-01-01T18:00:00-05:00",
is_daytime=True,
detailed_forecast="Rain likely with a high near 70. Chance of precipitation is 80%.",
detailed_forecast=(
"Rain likely with a high near 70. Chance of precipitation is 80%."
),
)

events = processor.extract_weather_events(period)
Expand Down
6 changes: 4 additions & 2 deletions tests/test_api_error_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,11 @@ async def test_handle_error_retry(error_handler):
# Second call calls retry_func again. It returns "Success".

# Wait, retry_func is called inside handle_error.
# First call to handle_error: context.retry_count=0. Increments to 1. Calls retry_func.
# First call to handle_error: context.retry_count=0. Increments to 1.
# Calls retry_func.
# retry_func raises. handle_error catches and recurses.
# Second call to handle_error: context.retry_count=1. Increments to 2. Calls retry_func.
# Second call to handle_error: context.retry_count=1. Increments to 2.
# Calls retry_func.
# retry_func returns "Success".

# So retry_func called twice.
Expand Down
21 changes: 16 additions & 5 deletions tests/test_api_formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ def sample_periods(self):
wind_speed="10 to 15 mph",
wind_direction="NW",
short_forecast="Sunny",
detailed_forecast="Sunny with a high near 75. Northwest wind 10 to 15 mph.",
detailed_forecast=(
"Sunny with a high near 75. Northwest wind 10 to 15 mph."
),
probability_of_precipitation=0,
),
ForecastPeriod(
Expand All @@ -44,7 +46,9 @@ def sample_periods(self):
wind_speed="5 to 10 mph",
wind_direction="NW",
short_forecast="Clear",
detailed_forecast="Clear with a low around 55. Northwest wind 5 to 10 mph.",
detailed_forecast=(
"Clear with a low around 55. Northwest wind 5 to 10 mph."
),
probability_of_precipitation=0,
),
]
Expand Down Expand Up @@ -151,7 +155,9 @@ def test_detect_period_events_rain(self, formatter, sample_events):
start_time="2024-01-01T06:00:00-05:00",
end_time="2024-01-01T18:00:00-05:00",
is_daytime=True,
detailed_forecast="Rain likely with a high near 70. Chance of precipitation is 80%.",
detailed_forecast=(
"Rain likely with a high near 70. Chance of precipitation is 80%."
),
)

events = formatter._detect_period_events(period, sample_events)
Expand Down Expand Up @@ -347,7 +353,10 @@ def test_clean_forecast_text(self, formatter):

def test_clean_forecast_for_display(self, formatter):
"""Test forecast text cleaning for display."""
text = "Sunny with a high near 75. Northwest wind 10 to 15 mph. Chance of precipitation is 0%."
text = (
"Sunny with a high near 75. Northwest wind 10 to 15 mph. "
"Chance of precipitation is 0%."
)
cleaned = formatter._clean_forecast_for_display(text)

# Should remove temperature and wind patterns
Expand Down Expand Up @@ -401,7 +410,9 @@ def test_format_compact_forecast_with_events(self, formatter):
temperature=75,
wind_speed="15 mph",
wind_direction="NW",
detailed_forecast="Rain likely with a high near 75. Northwest wind 15 mph.",
detailed_forecast=(
"Rain likely with a high near 75. Northwest wind 15 mph."
),
)
]
events = [
Expand Down
9 changes: 6 additions & 3 deletions tests/test_api_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ async def test_get_forecast_real_api(self, client):
assert "isDaytime" in first_period

print(
f"Retrieved forecast with {len(forecast_data['properties']['periods'])} periods"
f"Retrieved forecast with {len(forecast_data['properties']['periods'])} "
f"periods"
)

@pytest.mark.asyncio
Expand Down Expand Up @@ -245,8 +246,10 @@ async def test_caching_functionality(self, fetcher):

assert forecast1 == forecast2 # Should be identical

# Only assert timing if first call took significant time (indicating network request)
# If first call was extremely fast (< 10ms), it might have been cached externally or mocked
# Only assert timing if first call took significant time (indicating network
# request)
# If first call was extremely fast (< 10ms), it might have been cached
# externally or mocked
if first_call_time > 0.01:
assert second_call_time < first_call_time # Should be faster

Expand Down
86 changes: 56 additions & 30 deletions tests/test_compact_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,25 +33,35 @@ class TestCompactFormat:
@pytest.mark.skipif(not HAS_HA, reason="Home Assistant not available")
def test_compact_format_newlines(self):
"""Test that compact format preserves newlines between days."""
sample_forecast = """Tonight: Rain showers likely, mainly before 7am. Low around 52. Southeast wind 5 mph. Chance of precipitation is 80%.
Showers likely, mainly before 7am.
Tuesday: Rain showers likely, mainly before 7am. High near 64. Southeast wind 5 mph. Chance of precipitation is 60%.
Showers likely, mainly before 7am
Tuesday Night: Partly cloudy, with a low around 47. West wind 5 mph.
Wednesday: Partly sunny, with a high near 63. East wind 5 mph.
Wednesday Night: A chance of rain between 10pm and 1am. Low around 52. Southeast wind 5 to 10 mph. Chance of precipitation is 30%.
A chance of rain between 10pm and 1am
Thursday: A chance of rain between 7am and 1pm. High near 62. West wind 5 mph. Chance of precipitation is 30%.
A chance of rain between 7am and 1pm
Thursday Night: Mostly cloudy, with a low around 49.
Friday: A chance of rain after 10am. High near 65. Chance of precipitation is 30%.
A chance of rain after 10am
Friday Night: A chance of rain. Low around 50. Chance of precipitation is 30%.
A chance of rain
Saturday: A chance of rain. High near 63. Chance of precipitation is 30%.
A chance of rain
Saturday Night: A chance of rain. Low around 50. Chance of precipitation is 30%.
A chance of rain"""
sample_forecast = (
"Tonight: Rain showers likely, mainly before 7am. Low around 52. "
"Southeast wind 5 mph. Chance of precipitation is 80%.\n"
"Showers likely, mainly before 7am.\n"
"Tuesday: Rain showers likely, mainly before 7am. High near 64. "
"Southeast wind 5 mph. Chance of precipitation is 60%.\n"
"Showers likely, mainly before 7am\n"
"Tuesday Night: Partly cloudy, with a low around 47. West wind 5 mph.\n"
"Wednesday: Partly sunny, with a high near 63. East wind 5 mph.\n"
"Wednesday Night: A chance of rain between 10pm and 1am. Low around 52. "
"Southeast wind 5 to 10 mph. Chance of precipitation is 30%.\n"
"A chance of rain between 10pm and 1am\n"
"Thursday: A chance of rain between 7am and 1pm. High near 62. "
"West wind 5 mph. Chance of precipitation is 30%.\n"
"A chance of rain between 7am and 1pm\n"
"Thursday Night: Mostly cloudy, with a low around 49.\n"
"Friday: A chance of rain after 10am. High near 65. "
"Chance of precipitation is 30%.\n"
"A chance of rain after 10am\n"
"Friday Night: A chance of rain. Low around 50. "
"Chance of precipitation is 30%.\n"
"A chance of rain\n"
"Saturday: A chance of rain. High near 63. "
"Chance of precipitation is 30%.\n"
"A chance of rain\n"
"Saturday Night: A chance of rain. Low around 50. "
"Chance of precipitation is 30%.\n"
"A chance of rain"
)

# Format the compact forecast
compact_result = format_compact_forecast(sample_forecast)
Expand Down Expand Up @@ -88,9 +98,15 @@ def test_compact_format_newlines(self):

@pytest.mark.skipif(not HAS_HA, reason="Home Assistant not available")
def test_compact_format_no_pipe_separators(self):
"""Test that compact format doesn't use pipe separators that would trigger summary format detection."""
sample_forecast = """Tonight: Rain showers likely, mainly before 7am. Low around 52. Southeast wind 5 mph. Chance of precipitation is 80%.
Tuesday: Rain showers likely, mainly before 7am. High near 64. Southeast wind 5 mph. Chance of precipitation is 60%."""
"""Test that compact format doesn't use pipe separators that would
trigger summary format detection.
"""
sample_forecast = (
"Tonight: Rain showers likely, mainly before 7am. Low around 52. "
"Southeast wind 5 mph. Chance of precipitation is 80%.\n"
"Tuesday: Rain showers likely, mainly before 7am. High near 64. "
"Southeast wind 5 mph. Chance of precipitation is 60%."
)

# Format the compact forecast
compact_result = format_compact_forecast(sample_forecast)
Expand All @@ -107,11 +123,17 @@ def test_compact_format_no_pipe_separators(self):

@pytest.mark.skipif(not HAS_HA, reason="Home Assistant not available")
def test_compact_format_splitting(self):
"""Test that compact format is correctly detected and split by the split_message function."""
sample_forecast = """Tonight: Rain showers likely, mainly before 7am. Low around 52. Southeast wind 5 mph. Chance of precipitation is 80%.
Tuesday: Rain showers likely, mainly before 7am. High near 64. Southeast wind 5 mph. Chance of precipitation is 60%.
Tuesday Night: Partly cloudy, with a low around 47. West wind 5 mph.
Wednesday: Partly sunny, with a high near 63. East wind 5 mph."""
"""Test that compact format is correctly detected and split by the
split_message function.
"""
sample_forecast = (
"Tonight: Rain showers likely, mainly before 7am. Low around 52. "
"Southeast wind 5 mph. Chance of precipitation is 80%.\n"
"Tuesday: Rain showers likely, mainly before 7am. High near 64. "
"Southeast wind 5 mph. Chance of precipitation is 60%.\n"
"Tuesday Night: Partly cloudy, with a low around 47. West wind 5 mph.\n"
"Wednesday: Partly sunny, with a high near 63. East wind 5 mph."
)

# Format the compact forecast
compact_result = format_compact_forecast(sample_forecast)
Expand Down Expand Up @@ -159,9 +181,13 @@ def test_compact_format_splitting(self):
@pytest.mark.skipif(not HAS_HA, reason="Home Assistant not available")
def test_compact_format_day_separation(self):
"""Test that days are properly separated and don't run together."""
sample_forecast = """Tonight: Rain showers likely, mainly before 7am. Low around 52. Southeast wind 5 mph. Chance of precipitation is 80%.
Tuesday: Rain showers likely, mainly before 7am. High near 64. Southeast wind 5 mph. Chance of precipitation is 60%.
Tuesday Night: Partly cloudy, with a low around 47. West wind 5 mph."""
sample_forecast = (
"Tonight: Rain showers likely, mainly before 7am. Low around 52. "
"Southeast wind 5 mph. Chance of precipitation is 80%.\n"
"Tuesday: Rain showers likely, mainly before 7am. High near 64. "
"Southeast wind 5 mph. Chance of precipitation is 60%.\n"
"Tuesday Night: Partly cloudy, with a low around 47. West wind 5 mph."
)

# Format the compact forecast
compact_result = format_compact_forecast(sample_forecast)
Expand Down
6 changes: 4 additions & 2 deletions tests/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,13 +302,15 @@ async def test_options_flow_update(mock_hass, mock_config_entry):
"imap_folder": "INBOX",
}

# Mock validate_imap_folder (since folder didn't change, it shouldn't be called, but just in case)
# Mock validate_imap_folder (since folder didn't change, it shouldn't be called,
# but just in case)
mock_hass.async_add_executor_job.return_value = (True, None)

# Since we are using MockConfigFlow, async_create_entry is already defined there
# But we need to mock async_reload_entry import inside the method
# The method does: from . import async_reload_entry
# We can mock sys.modules['custom_components.satcom_forecast'] to have async_reload_entry
# We can mock sys.modules['custom_components.satcom_forecast'] to have
# async_reload_entry

async def mock_reload(*args, **kwargs):
pass
Expand Down
Loading