Skip to content

Commit

Permalink
feat: add sensor for last command and status
Browse files Browse the repository at this point in the history
  • Loading branch information
argoyle committed Jun 27, 2021
1 parent 6e657ca commit 47b9fee
Show file tree
Hide file tree
Showing 2 changed files with 292 additions and 12 deletions.
168 changes: 156 additions & 12 deletions custom_components/ferroamp/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@
SSO_TOPIC = "data/sso"
ESO_TOPIC = "data/eso"
ESM_TOPIC = "data/esm"
CONTROL_REQUEST_TOPIC = "control/request"
CONTROL_RESPONSE_TOPIC = "control/response"
CONTROL_RESULT_TOPIC = "control/result"

EHUB = "ehub"
EHUB_NAME = "EnergyHub"
Expand Down Expand Up @@ -100,6 +103,15 @@ async def async_setup_entry(
eso_sensors = {}
esm_sensors = {}
sso_sensors = {}
cmd_sensor = {}

def get_store(store_name):
store = config.get(store_name)
new = False
if store is None:
store = config[store_name] = {}
new = True
return store, new

def update_sensor_from_event(event, sensors, store):
for sensor in sensors:
Expand All @@ -116,9 +128,7 @@ def update_sensor_from_event(event, sensors, store):
@callback
def ehub_event_received(msg):
event = json.loads(msg.payload)
store = config.get(f"{slug}_{EHUB}")
if store is None:
store = config[f"{slug}_{EHUB}"] = {}
store, new = get_store(f"{slug}_{EHUB}")
update_sensor_from_event(event, ehub, store)

@callback
Expand All @@ -127,10 +137,9 @@ def sso_event_received(msg):
sso_id = event["id"]["val"]
device_id = f"{slug}_sso_{sso_id}"
device_name = f"{name} SSO {sso_id}"
store = config.get(device_id)
store, new = get_store(device_id)
sensors = sso_sensors.get(sso_id)
if store is None:
store = config[device_id] = {}
if new:
sensors = sso_sensors[sso_id] = [
VoltageFerroampSensor(
f"{device_name} PV String Voltage",
Expand Down Expand Up @@ -211,10 +220,9 @@ def eso_event_received(msg):
return
device_id = f"{slug}_eso_{eso_id}"
device_name = f"{name} ESO {eso_id}"
store = config.get(device_id)
store, new = get_store(device_id)
sensors = eso_sensors.get(eso_id)
if store is None:
store = config[device_id] = {}
if new:
sensors = eso_sensors[eso_id] = [
VoltageFerroampSensor(
f"{device_name} Battery Voltage",
Expand Down Expand Up @@ -312,10 +320,9 @@ def esm_event_received(msg):
esm_id = event["id"]["val"]
device_id = f"{slug}_esm_{esm_id}"
device_name = f"{name} ESM {esm_id}"
store = config.get(device_id)
store, new = get_store(device_id)
sensors = esm_sensors.get(esm_id)
if store is None:
store = config[device_id] = {}
if new:
sensors = esm_sensors[esm_id] = [
StringValFerroampSensor(
f"{device_name} Status",
Expand Down Expand Up @@ -359,6 +366,47 @@ def esm_event_received(msg):

update_sensor_from_event(event, sensors, store)

def get_cmd_sensor(store):
sensor = cmd_sensor.get('sensor')
if sensor is None:
sensor = CommandFerroampSensor(
f"{name} Control Status",
f"{slug}_{EHUB}",
f"{name} {EHUB_NAME}",
config_id
)
cmd_sensor['sensor'] = sensor
if sensor.unique_id not in store:
store[sensor.unique_id] = sensor
_LOGGER.debug(
"Registering new sensor %(unique_id)s",
dict(unique_id=sensor.unique_id),
)
async_add_entities((sensor,), True)
sensor.hass = hass
return sensor

@callback
def ehub_request_received(msg):
command = json.loads(msg.payload)
store, new = get_store(f"{slug}_{EHUB}")
sensor = get_cmd_sensor(store)
trans_id = command["transId"]
cmd = command["cmd"]
cmd_name = cmd["name"]
arg = cmd.get("arg")
sensor.add_request(trans_id, cmd_name, arg)

@callback
def ehub_response_received(msg):
response = json.loads(msg.payload)
store, new = get_store(f"{slug}_{EHUB}")
sensor = get_cmd_sensor(store)
trans_id = response["transId"]
status = response["status"]
message = response["msg"]
sensor.add_response(trans_id, status, message)

listeners.append(await mqtt.async_subscribe(
hass, f"{config_entry.data[CONF_PREFIX]}/{EHUB_TOPIC}", ehub_event_received, 0
))
Expand All @@ -371,6 +419,15 @@ def esm_event_received(msg):
listeners.append(await mqtt.async_subscribe(
hass, f"{config_entry.data[CONF_PREFIX]}/{ESM_TOPIC}", esm_event_received, 0
))
listeners.append(await mqtt.async_subscribe(
hass, f"{config_entry.data[CONF_PREFIX]}/{CONTROL_REQUEST_TOPIC}", ehub_request_received, 0
))
listeners.append(await mqtt.async_subscribe(
hass, f"{config_entry.data[CONF_PREFIX]}/{CONTROL_RESPONSE_TOPIC}", ehub_response_received, 0
))
listeners.append(await mqtt.async_subscribe(
hass, f"{config_entry.data[CONF_PREFIX]}/{CONTROL_RESULT_TOPIC}", ehub_response_received, 0
))

return True

Expand Down Expand Up @@ -783,6 +840,93 @@ def __init__(self, name, key, icon, device_id, device_name, interval, config_id)
super().__init__(name, key, POWER_WATT, icon, device_id, device_name, interval, 0, config_id)


class CommandFerroampSensor(RestoreEntity):
def __init__(self, name, device_id, device_name, config_id):
self._state = None
self._name = name
self._icon = "mdi:cog-transfer-outline"
self._device_id = device_id
self._device_name = device_name
self.config_id = config_id
self.updated = datetime.min
self.attrs = {}

@property
def name(self):
"""Return the name of the sensor."""
return self._name

@property
def unique_id(self):
"""Return unique ID of entity."""
return f"{self.device_id}_last_cmd"

@property
def icon(self):
return self._icon

@property
def device_id(self):
return self._device_id

@property
def device_info(self):
"""Return the device_info of the device."""
device_info = {
"identifiers": {(DOMAIN, self._device_id)},
"name": self._device_name,
"manufacturer": MANUFACTURER,
}
return device_info

@property
def state(self):
"""Return the state of the sensor."""
return self._state

@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return None

@property
def should_poll(self) -> bool:
return False

@property
def state_attributes(self):
return self.attrs

async def async_added_to_hass(self) -> None:
"""Handle entity which will be added."""
await super().async_added_to_hass()
state = await self.async_get_last_state()
if not state:
return
self._state = state.state
self.hass.data[DOMAIN][DATA_DEVICES][self.config_id][self.device_id][self.unique_id] = self

def add_request(self, trans_id, cmd, arg):
if arg is not None:
self._state = f"{cmd} ({arg})"
else:
self._state = cmd
self.attrs["transId"] = trans_id
self.attrs["status"] = None
self.attrs["message"] = None
self.updated = datetime.now()
if self.entity_id is not None:
self.async_write_ha_state()

def add_response(self, trans_id, status, message):
if self.attrs["transId"] == trans_id:
self.attrs["status"] = status
self.attrs["message"] = message
self.updated = datetime.now()
if self.entity_id is not None:
self.async_write_ha_state()


def ehub_sensors(slug, name, interval, precision_battery, precision_energy, precision_frequency, config_id):
return [
FloatValFerroampSensor(
Expand Down
136 changes: 136 additions & 0 deletions tests/test_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -932,3 +932,139 @@ async def test_base_class_update_state_from_events():
sensor = FerroampSensor("test", "key", "", "", "", "", 20, "a")
with pytest.raises(Exception):
sensor.update_state_from_events([{}])


async def test_control_command(hass, mqtt_mock):
config_entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_NAME: "Ferroamp",
CONF_PREFIX: "extapi"
},
options={
CONF_INTERVAL: 0
},
version=1,
unique_id="ferroamp",
)
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()

msg = """{
"transId": "abc-123",
"cmd": {"name": "charge", "arg": "5000"}
}"""
async_fire_mqtt_message(hass, "extapi/control/request", msg)
await hass.async_block_till_done()

state = hass.states.get("sensor.ferroamp_control_status")
assert state.state == "charge (5000)"
assert state.attributes == {
'friendly_name': 'Ferroamp Control Status',
'icon': 'mdi:cog-transfer-outline',
'transId': 'abc-123',
'status': None,
'message': None
}

async_fire_mqtt_message(hass, "extapi/control/request", msg)
await hass.async_block_till_done()

msg = """{
"transId": "xxx-123",
"status": "ack",
"msg": "some message"
}"""
async_fire_mqtt_message(hass, "extapi/control/response", msg)
await hass.async_block_till_done()

state = hass.states.get("sensor.ferroamp_control_status")
assert state.state == "charge (5000)"
assert state.attributes == {
'friendly_name': 'Ferroamp Control Status',
'icon': 'mdi:cog-transfer-outline',
'transId': 'abc-123',
'status': None,
'message': None
}

msg = """{
"transId": "abc-123",
"status": "ack",
"msg": "some message"
}"""
async_fire_mqtt_message(hass, "extapi/control/response", msg)
await hass.async_block_till_done()

state = hass.states.get("sensor.ferroamp_control_status")
assert state.state == "charge (5000)"
assert state.attributes == {
'friendly_name': 'Ferroamp Control Status',
'icon': 'mdi:cog-transfer-outline',
'transId': 'abc-123',
'status': "ack",
'message': "some message"
}

msg = """{
"transId": "abc-123",
"status": "nack",
"msg": "other message"
}"""
async_fire_mqtt_message(hass, "extapi/control/result", msg)
await hass.async_block_till_done()

state = hass.states.get("sensor.ferroamp_control_status")
assert state.state == "charge (5000)"
assert state.attributes == {
'friendly_name': 'Ferroamp Control Status',
'icon': 'mdi:cog-transfer-outline',
'transId': 'abc-123',
'status': "nack",
'message': "other message"
}


async def test_control_command_restore_state(hass, mqtt_mock):
mock_restore_cache(
hass,
(
State("sensor.ferroamp_control_status", "auto"),
),
)

hass.state = CoreState.starting

config_entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_NAME: "Ferroamp",
CONF_PREFIX: "extapi"
},
options={
CONF_INTERVAL: 0
},
version=1,
unique_id="ferroamp",
)
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()

msg = """{
"transId": "abc-123",
"cmd": {"name": "charge", "arg": "5000"}
}"""
async_fire_mqtt_message(hass, "extapi/control/request", msg)
await hass.async_block_till_done()

state = hass.states.get("sensor.ferroamp_control_status")
assert state.state == "auto"
assert state.attributes == {
'friendly_name': 'Ferroamp Control Status',
'icon': 'mdi:cog-transfer-outline',
'transId': 'abc-123',
'status': None,
'message': None
}

0 comments on commit 47b9fee

Please sign in to comment.