Fix ARTIK051_DONGLE_REF freezer type detection and door sensor (issue #77)
RZ32M713EWW/EE (an ARTIK051-dongle standalone freezer) reports no
oneUiVersion and a pipe-delimited modelNum
('ARTIK051_DONGLE_REF|<rest>') -- REF is the last underscore segment
before the pipe, not wrapped in underscores on both sides like the
'..._REF_...' shape for_device_by_model's substring check expected, so
the device fell through to 'unknown' with only common capabilities:
no door sensor, no temperature sensors, nothing fridge-specific.
Replace the substring check with a segment-based one
(_model_num_segments splits the pipe-delimited prefix on '_') that
catches both shapes. Same root cause, same fix, and same device family
independently reported and root-caused in issue #83 (which also covers
two further bugs -- config-flow/coordinator serial collisions on a
second symptom of this firmware, 'Nothing(SVC)' as a literal serial --
not needed here since this reporter has a single unit; left for #83).
Once routed to the refrigerator registry, fridge.py's existing pattern
capabilities pick up /temperature/current/freezer/0 and
/temperature/desired/freezer/0 for free -- they were never the actual
problem, just unreachable under the 'unknown' fallback (which never
tries pattern capabilities at all). The door sensor needed one more
fix: /door/onedoorfreezer/vs/0 reports the vendor-prefixed
x.com.samsung.da.openState, not the bare openState DOOR_GENERIC read,
so the entity existed but stayed permanently unavailable. Check both
field names via rep_fn.
This commit is contained in:
@@ -87,6 +87,22 @@ _CONSUMER_PREFIX_TO_KEY: dict[str, str] = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _model_num_segments(model_num: str) -> list[str]:
|
||||||
|
"""Underscore-delimited segments of modelNum's pipe-prefix.
|
||||||
|
|
||||||
|
Most boards wrap a token in underscores on both sides ('..._REF_...'),
|
||||||
|
which a plain substring check catches fine. But the ARTIK051_DONGLE_REF
|
||||||
|
family (issues #77, #83) reports modelNum as
|
||||||
|
'<board>_DONGLE_REF|<rest...>' -- REF is the *last* segment before the
|
||||||
|
pipe, with no trailing underscore, so '_REF_' never matches and the
|
||||||
|
device silently fell back to 'unknown'. Splitting on '_' and checking
|
||||||
|
segment membership catches both shapes without caring which side (if
|
||||||
|
either) has a delimiter.
|
||||||
|
"""
|
||||||
|
prefix = (model_num or '').split('|', 1)[0]
|
||||||
|
return prefix.split('_')
|
||||||
|
|
||||||
|
|
||||||
def for_device_by_model(model_num: str, description: str) -> Optional[DeviceRegistry]:
|
def for_device_by_model(model_num: str, description: str) -> Optional[DeviceRegistry]:
|
||||||
"""Fallback device-type detection for hardware that never reports
|
"""Fallback device-type detection for hardware that never reports
|
||||||
oneUiVersion (confirmed for washers -- their /otninformation/vs/0 has
|
oneUiVersion (confirmed for washers -- their /otninformation/vs/0 has
|
||||||
@@ -102,7 +118,7 @@ def for_device_by_model(model_num: str, description: str) -> Optional[DeviceRegi
|
|||||||
"""
|
"""
|
||||||
token = (description or '').split('/', 1)[0].rsplit('_', 1)[-1]
|
token = (description or '').split('/', 1)[0].rsplit('_', 1)[-1]
|
||||||
key = _CONSUMER_PREFIX_TO_KEY.get(token[:2].upper())
|
key = _CONSUMER_PREFIX_TO_KEY.get(token[:2].upper())
|
||||||
if key is None and '_REF_' in (model_num or ''):
|
if key is None and 'REF' in _model_num_segments(model_num):
|
||||||
key = 'refrigerator'
|
key = 'refrigerator'
|
||||||
# Room air conditioners (e.g. ARTIK051_PRAC_20K) report no oneUiVersion and
|
# Room air conditioners (e.g. ARTIK051_PRAC_20K) report no oneUiVersion and
|
||||||
# a modelNum carrying the '_PRAC_' (Package Room Air Conditioner) token.
|
# a modelNum carrying the '_PRAC_' (Package Room Air Conditioner) token.
|
||||||
|
|||||||
@@ -573,15 +573,27 @@ FLEX_ZONE = Capability(
|
|||||||
# Generic door pattern capability (href=None — use as pattern_cap only)
|
# Generic door pattern capability (href=None — use as pattern_cap only)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _door_open_state(rep):
|
||||||
|
"""Most /door/* resources report bare `openState`, but the
|
||||||
|
ARTIK051_DONGLE_REF family's /door/onedoorfreezer/vs/0 (issues #77, #83)
|
||||||
|
reports the vendor-prefixed `x.com.samsung.da.openState` instead. This
|
||||||
|
capability still binds either way (href_prefix match doesn't care about
|
||||||
|
field names), but a plain `field=` lookup against the wrong key means
|
||||||
|
the entity exists and is permanently unavailable -- check both."""
|
||||||
|
v = rep.get('openState')
|
||||||
|
if v is None:
|
||||||
|
v = rep.get('x.com.samsung.da.openState')
|
||||||
|
return v == 'Open'
|
||||||
|
|
||||||
|
|
||||||
DOOR_GENERIC = Capability(
|
DOOR_GENERIC = Capability(
|
||||||
href=None,
|
href=None,
|
||||||
href_prefix='/door/',
|
href_prefix='/door/',
|
||||||
poll_tier='hot',
|
poll_tier='hot',
|
||||||
entities=(
|
entities=(
|
||||||
BinarySensorDesc(key='open', field='openState',
|
BinarySensorDesc(key='open', rep_fn=_door_open_state,
|
||||||
translation_key='instance_open',
|
translation_key='instance_open',
|
||||||
use_instance_name=True, device_class='door',
|
use_instance_name=True, device_class='door'),
|
||||||
value_fn=lambda v: v == 'Open'),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"state_keys": [
|
||||||
|
"alarm_code",
|
||||||
|
"defrost_active",
|
||||||
|
"defrost_delay",
|
||||||
|
"diagnosis_status",
|
||||||
|
"door_onedoorfreezer_open",
|
||||||
|
"energy_kwh",
|
||||||
|
"energy_last_month_kwh",
|
||||||
|
"energy_saved_kwh",
|
||||||
|
"energy_this_month_kwh",
|
||||||
|
"firmware_update",
|
||||||
|
"freezer_setpoint",
|
||||||
|
"freezer_temperature",
|
||||||
|
"ice_maker_enabled",
|
||||||
|
"power_energy_kwh",
|
||||||
|
"power_watts",
|
||||||
|
"rapid_freezing",
|
||||||
|
"rapid_fridge",
|
||||||
|
"sabbath_mode"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
{
|
||||||
|
"device0": [
|
||||||
|
{
|
||||||
|
"rt": [
|
||||||
|
"x.com.samsung.devcol",
|
||||||
|
"oic.wk.col"
|
||||||
|
],
|
||||||
|
"if": [
|
||||||
|
"oic.if.baseline",
|
||||||
|
"oic.if.ll",
|
||||||
|
"oic.if.b"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/alarms/vs/0",
|
||||||
|
"rep": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/bespoke/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.BespokeProduct": "Off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/defrost/delay/0",
|
||||||
|
"rep": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/defrost/delay/vs/0",
|
||||||
|
"rep": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/diagnosis/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.diagnosisStart": "Ready"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/door/onedoorfreezer/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.openState": "Close"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/doors/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.items": [
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "3",
|
||||||
|
"x.com.samsung.da.description": "Door",
|
||||||
|
"x.com.samsung.da.openState": "Close"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/energy/consumption/vs/0",
|
||||||
|
"rep": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/file/information/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.timeoffset": "+02:00",
|
||||||
|
"x.com.samsung.supprtedtype": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/icemaker/status/0",
|
||||||
|
"rep": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/icemaker/status/vs/0",
|
||||||
|
"rep": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/information/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.modelNum": "ARTIK051_DONGLE_REF|00127641|00080020001430300100000000000000",
|
||||||
|
"x.com.samsung.da.description": "ARTIK_REF_17K",
|
||||||
|
"x.com.samsung.da.serialNum": "**REDACTED**",
|
||||||
|
"x.com.samsung.da.otnDUID": "**REDACTED**",
|
||||||
|
"x.com.samsung.da.items": [
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "0",
|
||||||
|
"x.com.samsung.da.description": "WiFi Module",
|
||||||
|
"x.com.samsung.da.type": "Software",
|
||||||
|
"x.com.samsung.da.number": "011120190711",
|
||||||
|
"x.com.samsung.da.newVersionAvailable": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "1",
|
||||||
|
"x.com.samsung.da.description": "Micom",
|
||||||
|
"x.com.samsung.da.type": "Firmware",
|
||||||
|
"x.com.samsung.da.number": "2103120d,ffffffff",
|
||||||
|
"x.com.samsung.da.newVersionAvailable": "0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/mode/0",
|
||||||
|
"rep": {
|
||||||
|
"supportedModes": [
|
||||||
|
"HOMECARE_WIZARD_V2",
|
||||||
|
"ENERGY_REPORT_MODEL",
|
||||||
|
"18K_REF_OUTDOOR_CONTROL_V2"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/mode/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.supportedModes": [
|
||||||
|
"HOMECARE_WIZARD_V2",
|
||||||
|
"ENERGY_REPORT_MODEL",
|
||||||
|
"18K_REF_OUTDOOR_CONTROL_V2"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/otninformation/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.target": "Micom",
|
||||||
|
"x.com.samsung.da.newVersionAvailable": "false"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/refrigeration/0",
|
||||||
|
"rep": {
|
||||||
|
"defrost": false,
|
||||||
|
"rapidFreeze": false,
|
||||||
|
"rapidCool": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/refrigeration/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.rapidFreezing": "Off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/sabbath/vs/0",
|
||||||
|
"rep": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/temperature/current/freezer/0",
|
||||||
|
"rep": {
|
||||||
|
"range": [
|
||||||
|
-23.0,
|
||||||
|
-15.0
|
||||||
|
],
|
||||||
|
"units": "C",
|
||||||
|
"temperature": -20.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/temperature/desired/freezer/0",
|
||||||
|
"rep": {
|
||||||
|
"range": [
|
||||||
|
-23.0,
|
||||||
|
-15.0
|
||||||
|
],
|
||||||
|
"units": "C",
|
||||||
|
"temperature": -20.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/temperatures/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.items": [
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "0",
|
||||||
|
"x.com.samsung.da.description": "Freezer",
|
||||||
|
"x.com.samsung.da.desired": "-20",
|
||||||
|
"x.com.samsung.da.current": "-20",
|
||||||
|
"x.com.samsung.da.maximum": "-15",
|
||||||
|
"x.com.samsung.da.minimum": "-23",
|
||||||
|
"x.com.samsung.da.unit": "Celsius"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -111,6 +111,25 @@ class TestWasherRegistry:
|
|||||||
assert href in registry.capabilities, f"{href} missing from washer registry"
|
assert href in registry.capabilities, f"{href} missing from washer registry"
|
||||||
|
|
||||||
|
|
||||||
|
class TestModelNumSegments:
|
||||||
|
def test_splits_pipe_prefix_on_underscore(self):
|
||||||
|
from custom_components.localthings.registry.by_type import _model_num_segments
|
||||||
|
assert _model_num_segments('ARTIK051_DONGLE_REF|00127641|000800200014') == [
|
||||||
|
'ARTIK051', 'DONGLE', 'REF',
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_ignores_everything_after_first_pipe(self):
|
||||||
|
from custom_components.localthings.registry.by_type import _model_num_segments
|
||||||
|
assert _model_num_segments('TP2X_RAC_20K|abc|REF_should_not_appear') == [
|
||||||
|
'TP2X', 'RAC', '20K',
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_empty_for_none_or_empty_input(self):
|
||||||
|
from custom_components.localthings.registry.by_type import _model_num_segments
|
||||||
|
assert _model_num_segments('') == ['']
|
||||||
|
assert _model_num_segments(None) == ['']
|
||||||
|
|
||||||
|
|
||||||
class TestForDeviceByModel:
|
class TestForDeviceByModel:
|
||||||
"""Fallback device-type detection for hardware without oneUiVersion."""
|
"""Fallback device-type detection for hardware without oneUiVersion."""
|
||||||
|
|
||||||
@@ -178,6 +197,20 @@ class TestForDeviceByModel:
|
|||||||
assert reg is not None
|
assert reg is not None
|
||||||
assert reg.name == 'refrigerator'
|
assert reg.name == 'refrigerator'
|
||||||
|
|
||||||
|
def test_refrigerator_dongle_ref_pipe_delimited_modelnum(self):
|
||||||
|
"""Issues #77/#83: the ARTIK051_DONGLE_REF family's modelNum is
|
||||||
|
'<board>_DONGLE_REF|<rest>' -- REF is the last underscore segment
|
||||||
|
before the pipe, with no trailing underscore, so the plain '_REF_'
|
||||||
|
substring check used to miss it entirely and the device fell back
|
||||||
|
to 'unknown' with only common capabilities."""
|
||||||
|
from custom_components.localthings.registry.by_type import for_device_by_model
|
||||||
|
reg = for_device_by_model(
|
||||||
|
'ARTIK051_DONGLE_REF|00127641|00080020001430300100000000000000',
|
||||||
|
'ARTIK_REF_17K',
|
||||||
|
)
|
||||||
|
assert reg is not None
|
||||||
|
assert reg.name == 'refrigerator'
|
||||||
|
|
||||||
def test_airconditioner_via_prac_token(self):
|
def test_airconditioner_via_prac_token(self):
|
||||||
"""Issue #17: a room AC (ARTIK051_PRAC_20K) reports no oneUiVersion and
|
"""Issue #17: a room AC (ARTIK051_PRAC_20K) reports no oneUiVersion and
|
||||||
an unrecognized consumer token ('20K'); it falls back to the '_PRAC_'
|
an unrecognized consumer token ('20K'); it falls back to the '_PRAC_'
|
||||||
|
|||||||
@@ -20,6 +20,29 @@ class TestTempCurrentGeneric:
|
|||||||
assert desc.unit_fn({'temperature': 5.0}) == '°F'
|
assert desc.unit_fn({'temperature': 5.0}) == '°F'
|
||||||
|
|
||||||
|
|
||||||
|
class TestDoorGeneric:
|
||||||
|
"""Issues #77/#83: DOOR_GENERIC used to read only the bare `openState`
|
||||||
|
field. Most /door/* resources use that, but the ARTIK051_DONGLE_REF
|
||||||
|
family's /door/onedoorfreezer/vs/0 reports the vendor-prefixed
|
||||||
|
`x.com.samsung.da.openState` instead -- the pattern capability still
|
||||||
|
bound the href either way, but the entity's value was always None."""
|
||||||
|
|
||||||
|
def test_reads_bare_open_state(self):
|
||||||
|
desc = fridge.DOOR_GENERIC.entities[0]
|
||||||
|
assert desc.rep_fn({'openState': 'Open'}) is True
|
||||||
|
assert desc.rep_fn({'openState': 'Close'}) is False
|
||||||
|
|
||||||
|
def test_reads_vendor_prefixed_open_state(self):
|
||||||
|
desc = fridge.DOOR_GENERIC.entities[0]
|
||||||
|
assert desc.rep_fn({'x.com.samsung.da.openState': 'Open'}) is True
|
||||||
|
assert desc.rep_fn({'x.com.samsung.da.openState': 'Close'}) is False
|
||||||
|
|
||||||
|
def test_prefers_bare_field_when_both_present(self):
|
||||||
|
desc = fridge.DOOR_GENERIC.entities[0]
|
||||||
|
rep = {'openState': 'Open', 'x.com.samsung.da.openState': 'Close'}
|
||||||
|
assert desc.rep_fn(rep) is True
|
||||||
|
|
||||||
|
|
||||||
class TestTempSetpointGeneric:
|
class TestTempSetpointGeneric:
|
||||||
def test_unit_reads_celsius(self):
|
def test_unit_reads_celsius(self):
|
||||||
desc = fridge.TEMP_SETPOINT_GENERIC.entities[0]
|
desc = fridge.TEMP_SETPOINT_GENERIC.entities[0]
|
||||||
@@ -231,6 +254,39 @@ class TestArtik051AndTp2xFixturesHaveCompleteCoverage:
|
|||||||
assert state['flex_zone_mode'] == 'CV_FDR_BEVERAGE'
|
assert state['flex_zone_mode'] == 'CV_FDR_BEVERAGE'
|
||||||
|
|
||||||
|
|
||||||
|
class TestArtik051DongleRefFixtureCoverage:
|
||||||
|
"""Issues #77/#83: the ARTIK051_DONGLE_REF standalone freezer resolves
|
||||||
|
to the refrigerator registry (segment-based modelNum detection) with
|
||||||
|
zero unbound hrefs, and its door/temperature entities -- which only
|
||||||
|
bind through pattern capabilities the 'unknown' fallback never tried --
|
||||||
|
now carry real values."""
|
||||||
|
|
||||||
|
def test_no_unbound_hrefs_and_expected_entities(self):
|
||||||
|
from custom_components.localthings.registry.adapter import flatten
|
||||||
|
from custom_components.localthings.registry.by_type import (
|
||||||
|
for_device_by_model, refrigerator,
|
||||||
|
)
|
||||||
|
from custom_components.localthings.registry.discovery import discover
|
||||||
|
from tests.conftest import _load_device
|
||||||
|
|
||||||
|
resources = _load_device('refrigerator_artik051_dongle_ref')
|
||||||
|
info = resources['/information/vs/0']
|
||||||
|
reg = for_device_by_model(
|
||||||
|
info['x.com.samsung.da.modelNum'], info['x.com.samsung.da.description'])
|
||||||
|
assert reg is not None and reg.name == 'refrigerator'
|
||||||
|
|
||||||
|
unbound = []
|
||||||
|
bound = discover(
|
||||||
|
resources, refrigerator.REGISTRY.capabilities,
|
||||||
|
refrigerator.REGISTRY.pattern_capabilities, log=unbound.append,
|
||||||
|
)
|
||||||
|
assert unbound == []
|
||||||
|
state = flatten(bound, resources)
|
||||||
|
assert state['door_onedoorfreezer_open'] is False # rep reports 'Close'
|
||||||
|
assert state['freezer_temperature'] == -20.0
|
||||||
|
assert state['freezer_setpoint'] == -20.0
|
||||||
|
|
||||||
|
|
||||||
class TestRefrigeratorAiEnergyLevelFixtureCoverage:
|
class TestRefrigeratorAiEnergyLevelFixtureCoverage:
|
||||||
"""AI energy-saving level (common.AI_ENERGY_LEVEL, see
|
"""AI energy-saving level (common.AI_ENERGY_LEVEL, see
|
||||||
test_common_capabilities.py) is exercised end-to-end here against the
|
test_common_capabilities.py) is exercised end-to-end here against the
|
||||||
|
|||||||
@@ -166,6 +166,27 @@ def test_registry_reproduces_golden_state_keys_for_artik051_ref_17k():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_registry_reproduces_golden_state_keys_for_artik051_dongle_ref():
|
||||||
|
"""ARTIK051_DONGLE_REF standalone freezer (issues #77/#83) -- reports no
|
||||||
|
oneUiVersion and a pipe-delimited modelNum ('..._DONGLE_REF|<rest>')
|
||||||
|
that the old '_REF_' substring check missed entirely; resolved via the
|
||||||
|
segment-based check in for_device_by_model. Its door
|
||||||
|
(/door/onedoorfreezer/vs/0) and temperature
|
||||||
|
(/temperature/{current,desired}/freezer/0) hrefs only bind through
|
||||||
|
fridge.py's pattern capabilities, which the 'unknown' fallback never
|
||||||
|
tries -- so this also regression-tests that those resources bind at
|
||||||
|
all once routed to the right registry."""
|
||||||
|
from tests.conftest import _load_device
|
||||||
|
resources = _load_device('refrigerator_artik051_dongle_ref')
|
||||||
|
golden = json.loads((GOLDEN / 'refrigerator_artik051_dongle_ref.json').read_text())
|
||||||
|
state_keys = _new_state_keys('refrigerator_artik051_dongle_ref', resources)
|
||||||
|
assert set(state_keys) == set(golden['state_keys']), (
|
||||||
|
f"state_keys mismatch:\n"
|
||||||
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
||||||
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_registry_reproduces_golden_state_keys_for_tp2x_ref_20k():
|
def test_registry_reproduces_golden_state_keys_for_tp2x_ref_20k():
|
||||||
"""TP2X_REF_20K -- CV_FDR_-prefixed flex zone (issue #32) plus the extra
|
"""TP2X_REF_20K -- CV_FDR_-prefixed flex zone (issue #32) plus the extra
|
||||||
energy fields (cumulativeConsumption/monthlyConsumption/
|
energy fields (cumulativeConsumption/monthlyConsumption/
|
||||||
|
|||||||
Reference in New Issue
Block a user