mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-03-30 05:13:40 +00:00
fix(services): correct period data format for ApexCharts visualization
Period data in array_of_arrays format now generates proper segment structure for stepline charts. Each period produces 2-3 data points depending on insert_nulls parameter: 1. Start time with price (begin period) 2. End time with price (hold price level) 3. End time with NULL (terminate segment, only if insert_nulls='segments'/'all') This enables ApexCharts to correctly display periods as continuous blocks with clean gaps between them. Previously only start point was generated, causing periods to render as single points instead of continuous segments. Changes: - formatters.py: Updated get_period_data() to generate 2-3 points per period - formatters.py: Added insert_nulls parameter to control NULL termination - get_chartdata.py: Pass insert_nulls parameter to get_period_data() - get_apexcharts_yaml.py: Set insert_nulls='segments' for period overlay - get_apexcharts_yaml.py: Preserve NULL values in data_generator mapping - get_apexcharts_yaml.py: Store original price for potential tooltip access - tests: Added comprehensive period data format tests Impact: Best price and peak price period overlays now display correctly as continuous blocks with proper segment separation in ApexCharts cards.
This commit is contained in:
parent
a3696fe182
commit
6e0310ef7c
4 changed files with 385 additions and 6 deletions
|
|
@ -215,6 +215,7 @@ def get_period_data( # noqa: PLR0913, PLR0912, PLR0915
|
||||||
level_field: str,
|
level_field: str,
|
||||||
rating_level_field: str,
|
rating_level_field: str,
|
||||||
data_key: str,
|
data_key: str,
|
||||||
|
insert_nulls: str,
|
||||||
add_trailing_null: bool,
|
add_trailing_null: bool,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
|
|
@ -243,6 +244,7 @@ def get_period_data( # noqa: PLR0913, PLR0912, PLR0915
|
||||||
level_field: Custom name for level field
|
level_field: Custom name for level field
|
||||||
rating_level_field: Custom name for rating_level field
|
rating_level_field: Custom name for rating_level field
|
||||||
data_key: Top-level key name in response
|
data_key: Top-level key name in response
|
||||||
|
insert_nulls: NULL insertion mode ('none', 'segments', 'all')
|
||||||
add_trailing_null: Whether to add trailing null point
|
add_trailing_null: Whether to add trailing null point
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
@ -327,7 +329,12 @@ def get_period_data( # noqa: PLR0913, PLR0912, PLR0915
|
||||||
chart_data.append(data_point)
|
chart_data.append(data_point)
|
||||||
|
|
||||||
else: # array_of_arrays
|
else: # array_of_arrays
|
||||||
# For array_of_arrays, include: [start, price_avg]
|
# For array_of_arrays, include 2-3 points per period depending on insert_nulls:
|
||||||
|
# Always:
|
||||||
|
# 1. Start time with price (begin period)
|
||||||
|
# 2. End time with price (hold price until end)
|
||||||
|
# If insert_nulls='segments' or 'all':
|
||||||
|
# 3. End time with NULL (cleanly terminate segment for ApexCharts)
|
||||||
price_avg = period.get("price_avg", 0.0)
|
price_avg = period.get("price_avg", 0.0)
|
||||||
# Convert to major currency unless minor_currency=True
|
# Convert to major currency unless minor_currency=True
|
||||||
if not minor_currency:
|
if not minor_currency:
|
||||||
|
|
@ -335,10 +342,23 @@ def get_period_data( # noqa: PLR0913, PLR0912, PLR0915
|
||||||
if round_decimals is not None:
|
if round_decimals is not None:
|
||||||
price_avg = round(price_avg, round_decimals)
|
price_avg = round(price_avg, round_decimals)
|
||||||
start = period["start"]
|
start = period["start"]
|
||||||
|
end = period.get("end")
|
||||||
start_serialized = start.isoformat() if hasattr(start, "isoformat") else start
|
start_serialized = start.isoformat() if hasattr(start, "isoformat") else start
|
||||||
chart_data.append([start_serialized, price_avg])
|
end_serialized = end.isoformat() if end and hasattr(end, "isoformat") else end
|
||||||
|
|
||||||
# Add trailing null point if requested
|
# Add data points per period
|
||||||
|
chart_data.append([start_serialized, price_avg]) # 1. Start with price
|
||||||
|
if end_serialized:
|
||||||
|
chart_data.append([end_serialized, price_avg]) # 2. End with price (hold level)
|
||||||
|
# 3. Add NULL terminator only if insert_nulls is enabled
|
||||||
|
if insert_nulls in ("segments", "all"):
|
||||||
|
chart_data.append([end_serialized, None]) # 3. End with NULL (terminate segment)
|
||||||
|
|
||||||
|
# Add trailing null point if requested (independent of insert_nulls)
|
||||||
|
# This adds an additional NULL at the end of the entire data series.
|
||||||
|
# If both insert_nulls and add_trailing_null are enabled, you get:
|
||||||
|
# - NULL terminator after each period (from insert_nulls)
|
||||||
|
# - Additional NULL at the very end (from add_trailing_null)
|
||||||
if add_trailing_null and chart_data:
|
if add_trailing_null and chart_data:
|
||||||
if output_format == "array_of_objects":
|
if output_format == "array_of_objects":
|
||||||
null_point = {start_time_field: None, end_time_field: None}
|
null_point = {start_time_field: None, end_time_field: None}
|
||||||
|
|
|
||||||
|
|
@ -284,6 +284,8 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
|
||||||
# Conditionally include day parameter (omit for rolling window mode)
|
# Conditionally include day parameter (omit for rolling window mode)
|
||||||
day_param = f"day: ['{day}'], " if day else ""
|
day_param = f"day: ['{day}'], " if day else ""
|
||||||
|
|
||||||
|
# Store original prices for tooltip, but map to 1 for full-height overlay
|
||||||
|
# We use a custom tooltip formatter to show the real price
|
||||||
best_price_generator = (
|
best_price_generator = (
|
||||||
f"const response = await hass.callWS({{ "
|
f"const response = await hass.callWS({{ "
|
||||||
f"type: 'call_service', "
|
f"type: 'call_service', "
|
||||||
|
|
@ -292,8 +294,13 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
|
||||||
f"return_response: true, "
|
f"return_response: true, "
|
||||||
f"service_data: {{ entry_id: '{entry_id}', {day_param}"
|
f"service_data: {{ entry_id: '{entry_id}', {day_param}"
|
||||||
f"period_filter: 'best_price', "
|
f"period_filter: 'best_price', "
|
||||||
f"output_format: 'array_of_arrays', minor_currency: true }} }}); "
|
f"output_format: 'array_of_arrays', insert_nulls: 'segments', minor_currency: true }} }}); "
|
||||||
f"return response.response.data.map(point => [point[0], 1]);"
|
f"const originalData = response.response.data; "
|
||||||
|
f"return originalData.map((point, i) => {{ "
|
||||||
|
f"const result = [point[0], point[1] === null ? null : 1]; "
|
||||||
|
f"result.originalPrice = point[1]; "
|
||||||
|
f"return result; "
|
||||||
|
f"}});"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Use first entity from entity_map (reuse existing entity to avoid extra header entries)
|
# Use first entity from entity_map (reuse existing entity to avoid extra header entries)
|
||||||
|
|
@ -305,7 +312,7 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
|
||||||
"name": best_price_name,
|
"name": best_price_name,
|
||||||
"type": "area",
|
"type": "area",
|
||||||
"color": "rgba(46, 204, 113, 0.2)", # Semi-transparent green
|
"color": "rgba(46, 204, 113, 0.2)", # Semi-transparent green
|
||||||
"yaxis_id": "highlight",
|
"yaxis_id": "highlight", # Use separate Y-axis (0-1) for full-height overlay
|
||||||
"show": {"legend_value": False, "in_header": False, "in_legend": False},
|
"show": {"legend_value": False, "in_header": False, "in_legend": False},
|
||||||
"data_generator": best_price_generator,
|
"data_generator": best_price_generator,
|
||||||
"stroke_width": 0,
|
"stroke_width": 0,
|
||||||
|
|
|
||||||
|
|
@ -213,6 +213,7 @@ async def handle_chartdata(call: ServiceCall) -> dict[str, Any]: # noqa: PLR091
|
||||||
level_field=level_field,
|
level_field=level_field,
|
||||||
rating_level_field=rating_level_field,
|
rating_level_field=rating_level_field,
|
||||||
data_key=data_key,
|
data_key=data_key,
|
||||||
|
insert_nulls=insert_nulls,
|
||||||
add_trailing_null=add_trailing_null,
|
add_trailing_null=add_trailing_null,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
351
tests/services/test_period_data_format.py
Normal file
351
tests/services/test_period_data_format.py
Normal file
|
|
@ -0,0 +1,351 @@
|
||||||
|
"""Test period data formatting for ApexCharts visualization."""
|
||||||
|
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
|
|
||||||
|
def test_period_array_of_arrays_with_insert_nulls() -> None:
|
||||||
|
"""
|
||||||
|
Test that period data generates 3 points per period when insert_nulls='segments'.
|
||||||
|
|
||||||
|
For ApexCharts to correctly display periods as continuous blocks:
|
||||||
|
1. Start time with price - Begin the period
|
||||||
|
2. End time with price - Hold the price level until end
|
||||||
|
3. End time with NULL - Cleanly terminate the segment (only with insert_nulls)
|
||||||
|
"""
|
||||||
|
# Simulate a period from formatters.get_period_data()
|
||||||
|
period = {
|
||||||
|
"start": datetime(2025, 12, 3, 10, 0, tzinfo=UTC),
|
||||||
|
"end": datetime(2025, 12, 3, 12, 0, tzinfo=UTC),
|
||||||
|
"price_avg": 1250, # Stored in minor units (12.50 EUR/ct)
|
||||||
|
"level": "CHEAP",
|
||||||
|
"rating_level": "LOW",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test with insert_nulls='segments' (should add NULL terminator)
|
||||||
|
chart_data = []
|
||||||
|
price_avg = period["price_avg"]
|
||||||
|
start_serialized = period["start"].isoformat()
|
||||||
|
end_serialized = period["end"].isoformat()
|
||||||
|
insert_nulls = "segments"
|
||||||
|
|
||||||
|
chart_data.append([start_serialized, price_avg]) # 1. Start with price
|
||||||
|
chart_data.append([end_serialized, price_avg]) # 2. End with price (hold level)
|
||||||
|
# 3. Add NULL terminator only if insert_nulls is enabled
|
||||||
|
if insert_nulls in ("segments", "all"):
|
||||||
|
chart_data.append([end_serialized, None]) # 3. End with NULL (terminate segment)
|
||||||
|
|
||||||
|
# Verify structure
|
||||||
|
assert len(chart_data) == 3, "Should generate 3 points with insert_nulls='segments'"
|
||||||
|
|
||||||
|
# Point 1: Start with price
|
||||||
|
assert chart_data[0][0] == "2025-12-03T10:00:00+00:00"
|
||||||
|
assert chart_data[0][1] == 1250
|
||||||
|
|
||||||
|
# Point 2: End with price (holds level)
|
||||||
|
assert chart_data[1][0] == "2025-12-03T12:00:00+00:00"
|
||||||
|
assert chart_data[1][1] == 1250
|
||||||
|
|
||||||
|
# Point 3: End with NULL (terminates segment)
|
||||||
|
assert chart_data[2][0] == "2025-12-03T12:00:00+00:00"
|
||||||
|
assert chart_data[2][1] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_period_array_of_arrays_without_insert_nulls() -> None:
|
||||||
|
"""
|
||||||
|
Test that period data generates 2 points per period when insert_nulls='none'.
|
||||||
|
|
||||||
|
Without NULL insertion, we only get:
|
||||||
|
1. Start time with price
|
||||||
|
2. End time with price
|
||||||
|
"""
|
||||||
|
period = {
|
||||||
|
"start": datetime(2025, 12, 3, 10, 0, tzinfo=UTC),
|
||||||
|
"end": datetime(2025, 12, 3, 12, 0, tzinfo=UTC),
|
||||||
|
"price_avg": 1250,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test with insert_nulls='none' (should NOT add NULL terminator)
|
||||||
|
chart_data = []
|
||||||
|
price_avg = period["price_avg"]
|
||||||
|
start_serialized = period["start"].isoformat()
|
||||||
|
end_serialized = period["end"].isoformat()
|
||||||
|
insert_nulls = "none"
|
||||||
|
|
||||||
|
chart_data.append([start_serialized, price_avg])
|
||||||
|
chart_data.append([end_serialized, price_avg])
|
||||||
|
if insert_nulls in ("segments", "all"):
|
||||||
|
chart_data.append([end_serialized, None])
|
||||||
|
|
||||||
|
# Verify structure: Only 2 points without NULL terminator
|
||||||
|
assert len(chart_data) == 2, "Should generate 2 points with insert_nulls='none'"
|
||||||
|
assert chart_data[0][1] == 1250
|
||||||
|
assert chart_data[1][1] == 1250
|
||||||
|
|
||||||
|
|
||||||
|
def test_multiple_periods_separated_by_nulls() -> None:
|
||||||
|
"""
|
||||||
|
Test that multiple periods are properly separated by NULL points with insert_nulls enabled.
|
||||||
|
|
||||||
|
This ensures gaps between periods are visualized correctly in ApexCharts.
|
||||||
|
"""
|
||||||
|
periods = [
|
||||||
|
{
|
||||||
|
"start": datetime(2025, 12, 3, 10, 0, tzinfo=UTC),
|
||||||
|
"end": datetime(2025, 12, 3, 12, 0, tzinfo=UTC),
|
||||||
|
"price_avg": 1250,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"start": datetime(2025, 12, 3, 15, 0, tzinfo=UTC),
|
||||||
|
"end": datetime(2025, 12, 3, 17, 0, tzinfo=UTC),
|
||||||
|
"price_avg": 1850,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
chart_data = []
|
||||||
|
insert_nulls = "segments"
|
||||||
|
for period in periods:
|
||||||
|
price_avg = period["price_avg"]
|
||||||
|
start_serialized = period["start"].isoformat()
|
||||||
|
end_serialized = period["end"].isoformat()
|
||||||
|
|
||||||
|
chart_data.append([start_serialized, price_avg])
|
||||||
|
chart_data.append([end_serialized, price_avg])
|
||||||
|
if insert_nulls in ("segments", "all"):
|
||||||
|
chart_data.append([end_serialized, None])
|
||||||
|
|
||||||
|
# Verify structure: 2 periods x 3 points = 6 total points (with insert_nulls)
|
||||||
|
assert len(chart_data) == 6, "Should generate 6 points for 2 periods with insert_nulls"
|
||||||
|
|
||||||
|
# Period 1 ends with NULL
|
||||||
|
assert chart_data[2][1] is None
|
||||||
|
|
||||||
|
# Period 2 starts
|
||||||
|
assert chart_data[3][0] == "2025-12-03T15:00:00+00:00"
|
||||||
|
assert chart_data[3][1] == 1850
|
||||||
|
|
||||||
|
# Period 2 ends with NULL
|
||||||
|
assert chart_data[5][1] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_multiple_periods_without_nulls() -> None:
|
||||||
|
"""
|
||||||
|
Test that multiple periods without insert_nulls generate continuous data.
|
||||||
|
|
||||||
|
Without NULL separators, periods connect directly (may be desired for some chart types).
|
||||||
|
"""
|
||||||
|
periods = [
|
||||||
|
{
|
||||||
|
"start": datetime(2025, 12, 3, 10, 0, tzinfo=UTC),
|
||||||
|
"end": datetime(2025, 12, 3, 12, 0, tzinfo=UTC),
|
||||||
|
"price_avg": 1250,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"start": datetime(2025, 12, 3, 15, 0, tzinfo=UTC),
|
||||||
|
"end": datetime(2025, 12, 3, 17, 0, tzinfo=UTC),
|
||||||
|
"price_avg": 1850,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
chart_data = []
|
||||||
|
insert_nulls = "none"
|
||||||
|
for period in periods:
|
||||||
|
price_avg = period["price_avg"]
|
||||||
|
start_serialized = period["start"].isoformat()
|
||||||
|
end_serialized = period["end"].isoformat()
|
||||||
|
|
||||||
|
chart_data.append([start_serialized, price_avg])
|
||||||
|
chart_data.append([end_serialized, price_avg])
|
||||||
|
if insert_nulls in ("segments", "all"):
|
||||||
|
chart_data.append([end_serialized, None])
|
||||||
|
|
||||||
|
# Verify structure: 2 periods x 2 points = 4 total points (without insert_nulls)
|
||||||
|
assert len(chart_data) == 4, "Should generate 4 points for 2 periods without insert_nulls"
|
||||||
|
|
||||||
|
# No NULL separators
|
||||||
|
assert all(point[1] is not None for point in chart_data)
|
||||||
|
|
||||||
|
|
||||||
|
def test_period_currency_conversion() -> None:
|
||||||
|
"""
|
||||||
|
Test that period prices are correctly converted between major/minor currency.
|
||||||
|
|
||||||
|
Period prices are stored in minor units (ct/øre) in coordinator data.
|
||||||
|
"""
|
||||||
|
period = {
|
||||||
|
"start": datetime(2025, 12, 3, 10, 0, tzinfo=UTC),
|
||||||
|
"end": datetime(2025, 12, 3, 12, 0, tzinfo=UTC),
|
||||||
|
"price_avg": 1250, # 12.50 ct/øre
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test 1: Keep minor currency (for ApexCharts internal use)
|
||||||
|
price_minor = period["price_avg"]
|
||||||
|
assert price_minor == 1250, "Should keep minor units"
|
||||||
|
|
||||||
|
# Test 2: Convert to major currency (for display)
|
||||||
|
price_major = period["price_avg"] / 100
|
||||||
|
assert price_major == 12.50, "Should convert to major units (EUR)"
|
||||||
|
|
||||||
|
|
||||||
|
def test_period_with_missing_end_time() -> None:
|
||||||
|
"""
|
||||||
|
Test handling of periods without end time (incomplete period).
|
||||||
|
|
||||||
|
If a period has no end time, we should only add the start point.
|
||||||
|
"""
|
||||||
|
period = {
|
||||||
|
"start": datetime(2025, 12, 3, 10, 0, tzinfo=UTC),
|
||||||
|
"end": None, # No end time
|
||||||
|
"price_avg": 1250,
|
||||||
|
}
|
||||||
|
|
||||||
|
chart_data = []
|
||||||
|
price_avg = period["price_avg"]
|
||||||
|
start_serialized = period["start"].isoformat()
|
||||||
|
end = period.get("end")
|
||||||
|
end_serialized = end.isoformat() if end else None
|
||||||
|
insert_nulls = "segments"
|
||||||
|
|
||||||
|
# Add start point
|
||||||
|
chart_data.append([start_serialized, price_avg])
|
||||||
|
|
||||||
|
# Only add end points if end_serialized exists
|
||||||
|
if end_serialized:
|
||||||
|
chart_data.append([end_serialized, price_avg])
|
||||||
|
if insert_nulls in ("segments", "all"):
|
||||||
|
chart_data.append([end_serialized, None])
|
||||||
|
|
||||||
|
# Verify: Only 1 point (start) for incomplete period
|
||||||
|
assert len(chart_data) == 1, "Should only have start point for incomplete period"
|
||||||
|
assert chart_data[0][1] == 1250
|
||||||
|
|
||||||
|
|
||||||
|
def test_apexcharts_mapping_preserves_structure() -> None:
|
||||||
|
"""
|
||||||
|
Test that ApexCharts .map() transformation preserves the 3-point structure.
|
||||||
|
|
||||||
|
The ApexCharts data_generator uses: .map(point => [point[0], 1])
|
||||||
|
This should preserve all 3 points but replace price with 1 (for overlay).
|
||||||
|
"""
|
||||||
|
# Simulate period data (3 points per period with insert_nulls='segments')
|
||||||
|
period_data = [
|
||||||
|
["2025-12-03T10:00:00+00:00", 1250], # Start with price
|
||||||
|
["2025-12-03T12:00:00+00:00", 1250], # End with price
|
||||||
|
["2025-12-03T12:00:00+00:00", None], # End with NULL
|
||||||
|
]
|
||||||
|
|
||||||
|
# Simulate ApexCharts mapping: [timestamp, 1] for overlay
|
||||||
|
mapped_data = [[point[0], 1 if point[1] is not None else None] for point in period_data]
|
||||||
|
|
||||||
|
# Verify structure is preserved
|
||||||
|
assert len(mapped_data) == 3, "Should preserve all 3 points"
|
||||||
|
assert mapped_data[0] == ["2025-12-03T10:00:00+00:00", 1] # Start
|
||||||
|
assert mapped_data[1] == ["2025-12-03T12:00:00+00:00", 1] # End (hold)
|
||||||
|
assert mapped_data[2] == ["2025-12-03T12:00:00+00:00", None] # End (terminate)
|
||||||
|
|
||||||
|
|
||||||
|
def test_insert_nulls_all_mode() -> None:
|
||||||
|
"""
|
||||||
|
Test that insert_nulls='all' also adds NULL terminators.
|
||||||
|
|
||||||
|
The 'all' mode should behave the same as 'segments' for period data.
|
||||||
|
"""
|
||||||
|
period = {
|
||||||
|
"start": datetime(2025, 12, 3, 10, 0, tzinfo=UTC),
|
||||||
|
"end": datetime(2025, 12, 3, 12, 0, tzinfo=UTC),
|
||||||
|
"price_avg": 1250,
|
||||||
|
}
|
||||||
|
|
||||||
|
chart_data = []
|
||||||
|
price_avg = period["price_avg"]
|
||||||
|
start_serialized = period["start"].isoformat()
|
||||||
|
end_serialized = period["end"].isoformat()
|
||||||
|
insert_nulls = "all"
|
||||||
|
|
||||||
|
chart_data.append([start_serialized, price_avg])
|
||||||
|
chart_data.append([end_serialized, price_avg])
|
||||||
|
if insert_nulls in ("segments", "all"):
|
||||||
|
chart_data.append([end_serialized, None])
|
||||||
|
|
||||||
|
# Verify: 3 points with insert_nulls='all'
|
||||||
|
assert len(chart_data) == 3, "Should generate 3 points with insert_nulls='all'"
|
||||||
|
assert chart_data[2][1] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_insert_nulls_and_add_trailing_null_both_enabled() -> None:
|
||||||
|
"""
|
||||||
|
Test that both insert_nulls and add_trailing_null work together correctly.
|
||||||
|
|
||||||
|
When both are enabled, you should get:
|
||||||
|
- NULL terminator after each period (from insert_nulls)
|
||||||
|
- Additional NULL at the very end (from add_trailing_null)
|
||||||
|
|
||||||
|
This results in TWO NULL points at the end: one for the last period, one trailing.
|
||||||
|
"""
|
||||||
|
periods = [
|
||||||
|
{
|
||||||
|
"start": datetime(2025, 12, 3, 10, 0, tzinfo=UTC),
|
||||||
|
"end": datetime(2025, 12, 3, 12, 0, tzinfo=UTC),
|
||||||
|
"price_avg": 1250,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
chart_data = []
|
||||||
|
insert_nulls = "segments"
|
||||||
|
add_trailing_null = True
|
||||||
|
|
||||||
|
for period in periods:
|
||||||
|
price_avg = period["price_avg"]
|
||||||
|
start_serialized = period["start"].isoformat()
|
||||||
|
end_serialized = period["end"].isoformat()
|
||||||
|
|
||||||
|
chart_data.append([start_serialized, price_avg])
|
||||||
|
chart_data.append([end_serialized, price_avg])
|
||||||
|
if insert_nulls in ("segments", "all"):
|
||||||
|
chart_data.append([end_serialized, None])
|
||||||
|
|
||||||
|
# Add trailing null
|
||||||
|
if add_trailing_null:
|
||||||
|
chart_data.append([None, None])
|
||||||
|
|
||||||
|
# Verify: 3 points (period) + 1 trailing = 4 total
|
||||||
|
assert len(chart_data) == 4, "Should have 4 points with both insert_nulls and add_trailing_null"
|
||||||
|
|
||||||
|
# Last period's NULL terminator
|
||||||
|
assert chart_data[2][0] == "2025-12-03T12:00:00+00:00"
|
||||||
|
assert chart_data[2][1] is None
|
||||||
|
|
||||||
|
# Trailing NULL (completely null)
|
||||||
|
assert chart_data[3][0] is None
|
||||||
|
assert chart_data[3][1] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_neither_insert_nulls_nor_add_trailing_null() -> None:
|
||||||
|
"""
|
||||||
|
Test that when both insert_nulls='none' and add_trailing_null=False, no NULLs are added.
|
||||||
|
|
||||||
|
This gives clean period data without any NULL separators.
|
||||||
|
"""
|
||||||
|
period = {
|
||||||
|
"start": datetime(2025, 12, 3, 10, 0, tzinfo=UTC),
|
||||||
|
"end": datetime(2025, 12, 3, 12, 0, tzinfo=UTC),
|
||||||
|
"price_avg": 1250,
|
||||||
|
}
|
||||||
|
|
||||||
|
chart_data = []
|
||||||
|
price_avg = period["price_avg"]
|
||||||
|
start_serialized = period["start"].isoformat()
|
||||||
|
end_serialized = period["end"].isoformat()
|
||||||
|
insert_nulls = "none"
|
||||||
|
add_trailing_null = False
|
||||||
|
|
||||||
|
chart_data.append([start_serialized, price_avg])
|
||||||
|
chart_data.append([end_serialized, price_avg])
|
||||||
|
if insert_nulls in ("segments", "all"):
|
||||||
|
chart_data.append([end_serialized, None])
|
||||||
|
|
||||||
|
if add_trailing_null:
|
||||||
|
chart_data.append([None, None])
|
||||||
|
|
||||||
|
# Verify: Only 2 points (start, end) without any NULLs
|
||||||
|
assert len(chart_data) == 2, "Should have 2 points without NULL insertion"
|
||||||
|
assert all(point[1] is not None for point in chart_data), "No NULL values should be present"
|
||||||
Loading…
Reference in a new issue