feat: replace completion_time/completion_minutes with finish_time timestamp
Removes two redundant sensors (a raw H:MM:SS string and a duration integer) in favour of a single SensorDeviceClass.TIMESTAMP that shows the absolute estimated finish time. The HA frontend auto-renders this as "in X minutes" and keeps it current without re-polling. Also removes dead _project / _on_observation / _rem_minutes code from both operational.py and oven.py — these hooks were declared on the Capability but never called anywhere in the coordinator or adapter.
This commit is contained in:
@@ -1,9 +1,8 @@
|
||||
"""Operational-state capability: machine state, progress, remaining-time.
|
||||
|
||||
Carries the rare cross-field hooks (active_when, on_observation, project) for
|
||||
remaining-time extrapolation. Shared by dryer/dishwasher/oven/washer families.
|
||||
Shared by dryer/dishwasher/oven/washer families.
|
||||
"""
|
||||
import time
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
from ..capability import Capability
|
||||
from ..entities import BinarySensorDesc, ButtonDesc, SensorDesc
|
||||
@@ -33,38 +32,15 @@ def _active_when(rep):
|
||||
return _SAMSUNG_STATE_TO_OCF.get(rep.get('x.com.samsung.da.state')) == 'active'
|
||||
|
||||
|
||||
def _on_observation(state, rep):
|
||||
rem = rep.get('x.com.samsung.da.remainingTime')
|
||||
if not isinstance(rem, str):
|
||||
return
|
||||
try:
|
||||
h, m, s = rem.split(':')
|
||||
state['remaining_anchor'] = (time.time(),
|
||||
int(h) * 3600 + int(m) * 60 + int(s))
|
||||
except (ValueError, AttributeError):
|
||||
pass
|
||||
|
||||
|
||||
def _project(state, sensors):
|
||||
anchor = state.get('remaining_anchor')
|
||||
if sensors.get('machine_state') != 'active' or anchor is None:
|
||||
return sensors
|
||||
ts, total = anchor
|
||||
remaining = max(0, int(total - (time.time() - ts)))
|
||||
h, rest = divmod(remaining, 3600)
|
||||
m, s = divmod(rest, 60)
|
||||
sensors = dict(sensors)
|
||||
sensors['completion_time'] = f"{h}:{m:02d}:{s:02d}"
|
||||
sensors['completion_minutes'] = h * 60 + m + (1 if s > 0 else 0)
|
||||
return sensors
|
||||
|
||||
|
||||
def _rem_minutes(remaining):
|
||||
if not remaining:
|
||||
def _finish_time(remaining_str):
|
||||
if not remaining_str:
|
||||
return None
|
||||
try:
|
||||
h, m, s = remaining.split(':')
|
||||
return int(h) * 60 + int(m) + (1 if int(s) > 0 else 0)
|
||||
h, m, s = remaining_str.split(':')
|
||||
total_s = int(h) * 3600 + int(m) * 60 + int(s)
|
||||
if total_s == 0:
|
||||
return None
|
||||
return datetime.now(timezone.utc) + timedelta(seconds=total_s)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@@ -87,12 +63,9 @@ OPERATIONAL_STATE = Capability(
|
||||
field='x.com.samsung.da.progressPercentage',
|
||||
name='Progress percent', unit='%', state_class='measurement',
|
||||
value_fn=_int),
|
||||
SensorDesc(key='completion_time', field='x.com.samsung.da.remainingTime',
|
||||
name='Completion time', icon='mdi:timer-sand'),
|
||||
SensorDesc(key='completion_minutes', field='x.com.samsung.da.remainingTime',
|
||||
name='Remaining minutes', unit='min', device_class='duration',
|
||||
state_class='measurement',
|
||||
value_fn=lambda r: _rem_minutes(r)),
|
||||
SensorDesc(key='finish_time', field='x.com.samsung.da.remainingTime',
|
||||
name='Estimated finish', device_class='timestamp',
|
||||
value_fn=_finish_time),
|
||||
SensorDesc(key='delay_start_time', field='x.com.samsung.da.delayStartTime',
|
||||
name='Delay start time', icon='mdi:timer-pause'),
|
||||
ButtonDesc(key='start', field='', name='Start cycle', payload='Run',
|
||||
@@ -112,6 +85,4 @@ OPERATIONAL_STATE = Capability(
|
||||
{'x.com.samsung.da.state': p})),
|
||||
),
|
||||
active_when=_active_when,
|
||||
on_observation=_on_observation,
|
||||
project=_project,
|
||||
)
|
||||
|
||||
@@ -22,7 +22,7 @@ _start_open.md). Mode writes are also unreliable — the oven rolls them back
|
||||
once a cycle is active. OVEN_MODE is provided as a SelectDesc for fidelity
|
||||
but is effectively read-only in practice.
|
||||
"""
|
||||
import time
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
from ..capability import Capability
|
||||
from ..entities import (
|
||||
@@ -76,12 +76,15 @@ def _int(v):
|
||||
return None
|
||||
|
||||
|
||||
def _rem_minutes(remaining):
|
||||
if not remaining:
|
||||
def _finish_time(remaining_str):
|
||||
if not remaining_str:
|
||||
return None
|
||||
try:
|
||||
h, m, s = remaining.split(':')
|
||||
return int(h) * 60 + int(m) + (1 if int(s) > 0 else 0)
|
||||
h, m, s = remaining_str.split(':')
|
||||
total_s = int(h) * 3600 + int(m) * 60 + int(s)
|
||||
if total_s == 0:
|
||||
return None
|
||||
return datetime.now(timezone.utc) + timedelta(seconds=total_s)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@@ -211,39 +214,10 @@ def _naturalsteam_write(p, rep, href=None):
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# on_observation / project for remaining-time extrapolation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _active_when(rep):
|
||||
return _SAMSUNG_STATE_TO_OCF.get(rep.get('x.com.samsung.da.state')) == 'active'
|
||||
|
||||
|
||||
def _on_observation(state, rep):
|
||||
rem = rep.get('x.com.samsung.da.remainingTime')
|
||||
if not isinstance(rem, str):
|
||||
return
|
||||
try:
|
||||
h, m, s = rem.split(':')
|
||||
state['remaining_anchor'] = (time.time(),
|
||||
int(h) * 3600 + int(m) * 60 + int(s))
|
||||
except (ValueError, AttributeError):
|
||||
pass
|
||||
|
||||
|
||||
def _project(state, sensors):
|
||||
anchor = state.get('remaining_anchor')
|
||||
if sensors.get('machine_state') != 'active' or anchor is None:
|
||||
return sensors
|
||||
ts, total = anchor
|
||||
remaining = max(0, int(total - (time.time() - ts)))
|
||||
h, rest = divmod(remaining, 3600)
|
||||
m, s = divmod(rest, 60)
|
||||
sensors = dict(sensors)
|
||||
sensors['completion_time'] = f"{h}:{m:02d}:{s:02d}"
|
||||
sensors['completion_minutes'] = h * 60 + m + (1 if s > 0 else 0)
|
||||
return sensors
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Capabilities
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -252,8 +226,6 @@ OVEN_OPERATIONAL_STATE = Capability(
|
||||
href='/operational/state/vs/0',
|
||||
poll_tier='hot',
|
||||
active_when=_active_when,
|
||||
on_observation=_on_observation,
|
||||
project=_project,
|
||||
entities=(
|
||||
SensorDesc(key='machine_state', field='x.com.samsung.da.state',
|
||||
name='Machine state', icon='mdi:stove', value_fn=_to_ocf),
|
||||
@@ -268,11 +240,9 @@ OVEN_OPERATIONAL_STATE = Capability(
|
||||
field='x.com.samsung.da.operationTime',
|
||||
name='Operation time (minutes)', unit='min',
|
||||
state_class='measurement', value_fn=_op_minutes),
|
||||
SensorDesc(key='completion_time', field='x.com.samsung.da.remainingTime',
|
||||
name='Completion time', icon='mdi:timer-sand'),
|
||||
SensorDesc(key='completion_minutes', field='x.com.samsung.da.remainingTime',
|
||||
name='Remaining minutes', unit='min', device_class='duration',
|
||||
state_class='measurement', value_fn=_rem_minutes),
|
||||
SensorDesc(key='finish_time', field='x.com.samsung.da.remainingTime',
|
||||
name='Estimated finish', device_class='timestamp',
|
||||
value_fn=_finish_time),
|
||||
NumberDesc(key='cook_time', field='x.com.samsung.da.operationTime',
|
||||
name='Cook time', unit='min', native_min=0, native_max=1439,
|
||||
step=1.0, icon='mdi:timer', value_fn=_op_minutes,
|
||||
|
||||
Reference in New Issue
Block a user