Skip to content

Commit d7996e8

Browse files
MaStrclaude
andcommitted
Address fifth Copilot review: thread safety, MQTT precision, integer timestamps
override_manager.py: - get_override() now returns a defensive copy via dataclasses.replace() instead of the live internal OverrideState instance; callers (including other threads) can no longer mutate expires_at/charge_rate without holding the manager lock, preserving the thread-safety guarantee mqtt_api.py: - publish_override_duration() changed from :.0f to :.1f so that fractional-minute durations (e.g. 45.5) are published accurately instead of being silently rounded to the nearest whole minute mcp_server.py: - _format_forecast_array() now uses integer floor division for base_time (int(run_time) // interval_seconds * interval_seconds) and casts each time_start to int, eliminating floating-point drift in JSON timestamps All 373 tests pass; pylint 9.46/10. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 00e1835 commit d7996e8

3 files changed

Lines changed: 7 additions & 6 deletions

File tree

src/batcontrol/mcp_server.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,12 @@ def _format_forecast_array(arr, run_time: float, interval_minutes: int,
6060
if arr is None:
6161
return []
6262
interval_seconds = interval_minutes * 60
63-
base_time = run_time - (run_time % interval_seconds)
63+
base_time = (int(run_time) // interval_seconds) * interval_seconds
6464
result = []
6565
for i, val in enumerate(arr):
6666
result.append({
6767
'slot': i,
68-
'time_start': base_time + i * interval_seconds,
68+
'time_start': int(base_time + i * interval_seconds),
6969
'value': round(float(val), digits),
7070
})
7171
return result

src/batcontrol/mqtt_api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -464,7 +464,7 @@ def publish_override_duration(self, duration_minutes: float) -> None:
464464
if self.client.is_connected():
465465
self.client.publish(
466466
self.base_topic + '/override_duration',
467-
f'{duration_minutes:.0f}')
467+
f'{duration_minutes:.1f}')
468468

469469
def publish_production_offset(self, production_offset: float) -> None:
470470
""" Publish the production offset percentage to MQTT

src/batcontrol/override_manager.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import time
1010
import threading
1111
import logging
12-
from dataclasses import dataclass, field
12+
from dataclasses import dataclass, field, replace
1313
from typing import Optional
1414

1515
logger = logging.getLogger(__name__)
@@ -117,8 +117,9 @@ def clear_override(self) -> None:
117117
self._override = None
118118

119119
def get_override(self) -> Optional[OverrideState]:
120-
"""Get the active override, or None if no override is active.
120+
"""Get a snapshot of the active override, or None if no override is active.
121121
122+
Returns a defensive copy so callers cannot mutate the internal state.
122123
Automatically clears expired overrides.
123124
"""
124125
with self._lock:
@@ -131,7 +132,7 @@ def get_override(self) -> Optional[OverrideState]:
131132
)
132133
self._override = None
133134
return None
134-
return self._override
135+
return replace(self._override)
135136

136137
def is_active(self) -> bool:
137138
"""Check if an override is currently active (not expired)."""

0 commit comments

Comments
 (0)