Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CM10 Sensor, Boot speed and Charge Peak attributes #52

Merged
merged 12 commits into from
Jan 23, 2024
131 changes: 116 additions & 15 deletions custom_components/checkwatt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from datetime import time, timedelta
import logging
import random
import re
from typing import TypedDict

import aiohttp
Expand All @@ -20,6 +21,7 @@

from .const import (
BASIC_TEST,
CONF_CM10_SENSOR,
CONF_DETAILED_SENSORS,
CONF_PUSH_CW_TO_RANK,
CONF_UPDATE_INTERVAL,
Expand Down Expand Up @@ -53,7 +55,7 @@ class CheckwattResp(TypedDict):
update_time: str
next_update_time: str
fcr_d_status: str
fcr_d_state: str
fcr_d_info: str
fcr_d_date: str
total_solar_energy: float
total_charging_energy: float
Expand All @@ -70,6 +72,12 @@ class CheckwattResp(TypedDict):
battery_soc: float
dso: str
energy_provider: str
cm10_status: str
cm10_version: str
charge_peak_ac: float
charge_peak_dc: float
discharge_peak_ac: float
discharge_peak_dc: float


async def update_listener(hass: HomeAssistant, entry):
Expand Down Expand Up @@ -98,6 +106,65 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return unload_ok


async def getPeakData(cw_inst):
"""Extract PeakAcDC Power."""
charge_peak_ac = 0
charge_peak_dc = 0
discharge_peak_ac = 0
discharge_peak_dc = 0

if cw_inst is None:
return (None, None, None, None)

if cw_inst.customer_details is None:
return (None, None, None, None)

if "Meter" in cw_inst.customer_details:
for meter in cw_inst.customer_details["Meter"]:
if "InstallationType" in meter:
if meter["InstallationType"] == "Charging":
if "PeakAcKw" in meter and "PeakDcKw" in meter:
charge_peak_ac += meter["PeakAcKw"]
charge_peak_dc += meter["PeakDcKw"]
if meter["InstallationType"] == "Discharging":
if "PeakAcKw" in meter and "PeakDcKw" in meter:
discharge_peak_ac += meter["PeakAcKw"]
discharge_peak_dc += meter["PeakDcKw"]

return (charge_peak_ac, charge_peak_dc, discharge_peak_ac, discharge_peak_dc)


def extract_fcrd_status(cw_inst):
"""Extract status from data and logbook."""

if cw_inst.customer_details is None:
return (None, None, None)

pattern = re.compile(
r"\[ FCR-D (ACTIVATED|DEACTIVATE|FAIL ACTIVATION) \](?:.*?(\d+,\d+/\d+,\d+/\d+,\d+ %))?(?:\s*(.*?))?(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})"
)
for entry in cw_inst.logbook_entries:
match = pattern.search(entry)
if match:
fcrd_state = match.group(1)
fcrd_percentage = (
match.group(2)
if fcrd_state in ["ACTIVATED", "FAIL ACTIVATION"]
else None
)
error_info = match.group(3) if fcrd_state == "DEACTIVATE" else None
fcrd_timestamp = match.group(4)
if fcrd_percentage is not None:
fcrd_info = fcrd_percentage
elif error_info is not None:
fcrd_info = error_info
else:
fcrd_info = None
break

return (fcrd_state, fcrd_info, fcrd_timestamp)


class CheckwattCoordinator(DataUpdateCoordinator[CheckwattResp]):
"""Data update coordinator."""

Expand Down Expand Up @@ -125,9 +192,10 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
self.energy_provider = None
self.random_offset = random.randint(0, 14)
self.fcrd_state = None
self.fcrd_percentage = None
self.fcrd_info = None
self.fcrd_timestamp = None
self._id = None
self.update_no = 0
_LOGGER.debug("Fetching annual revenue at 3:%02d am", self.random_offset)

@property
Expand All @@ -143,6 +211,7 @@ async def _async_update_data(self) -> CheckwattResp: # noqa: C901
password = self._entry.data.get(CONF_PASSWORD)
use_detailed_sensors = self._entry.options.get(CONF_DETAILED_SENSORS)
push_to_cw_rank = self._entry.options.get(CONF_PUSH_CW_TO_RANK)
use_cm10_sensor = self._entry.options.get(CONF_CM10_SENSOR)

async with CheckwattManager(
username, password, INTEGRATION_NAME
Expand All @@ -153,14 +222,26 @@ async def _async_update_data(self) -> CheckwattResp: # noqa: C901
if not await cw_inst.get_customer_details():
_LOGGER.error("Failed to obtain customer details, abort update")
raise UpdateFailed("Unknown error get_customer_details")
if use_cm10_sensor:
if not await cw_inst.get_meter_status():
_LOGGER.error("Failed to obtain meter details, abort update")
raise UpdateFailed("Unknown error get_meter_status")
if not await cw_inst.get_energy_flow():
_LOGGER.error("Failed to get energy flows, abort update")
raise UpdateFailed("Unknown error get_energy_flow")

(
fcrd_state,
fcrd_info,
fcrd_timestamp,
) = extract_fcrd_status(cw_inst)

# Prevent slow funcion to be called at boot.
# The revenue sensors will be updated after ca 1 min
if self.is_boot:
self.update_no += 1
if self.update_no > 2:
self.is_boot = False
if self.is_boot:
if (
"Meter" in cw_inst.customer_details
and len(cw_inst.customer_details["Meter"]) > 0
Expand All @@ -171,9 +252,7 @@ async def _async_update_data(self) -> CheckwattResp: # noqa: C901
)

# Store fcrd_state at boot, used to spark event
self.fcrd_state = cw_inst.fcrd_state
self.fcrd_percentage = cw_inst.fcrd_percentage
self.fcrd_timestamp = cw_inst.fcrd_timestamp
self.fcrd_state = fcrd_state
self._id = cw_inst.customer_details["Id"]

else:
Expand Down Expand Up @@ -238,9 +317,9 @@ async def _async_update_data(self) -> CheckwattResp: # noqa: C901
"display_name": cw_inst.customer_details["Meter"][0]["DisplayName"],
"update_time": self.update_time,
"next_update_time": self.next_update_time,
"fcr_d_status": cw_inst.fcrd_state,
"fcr_d_state": cw_inst.fcrd_percentage,
"fcr_d_date": cw_inst.fcrd_timestamp,
"fcr_d_status": fcrd_state,
"fcr_d_info": fcrd_info,
"fcr_d_date": fcrd_timestamp,
"battery_charge_peak": cw_inst.battery_charge_peak,
"battery_discharge_peak": cw_inst.battery_discharge_peak,
"dso": cw_inst.battery_registration["Dso"],
Expand All @@ -251,6 +330,16 @@ async def _async_update_data(self) -> CheckwattResp: # noqa: C901
resp["grid_power"] = cw_inst.grid_power
resp["solar_power"] = cw_inst.solar_power
resp["battery_soc"] = cw_inst.battery_soc
(
charge_peak_ac,
charge_peak_dc,
discharge_peak_ac,
discharge_peak_dc,
) = await getPeakData(cw_inst)
resp["charge_peak_ac"] = charge_peak_ac
resp["charge_peak_dc"] = charge_peak_dc
resp["discharge_peak_ac"] = discharge_peak_ac
resp["discharge_peak_dc"] = discharge_peak_dc

# Use self stored variant of revenue parameters as they are not always fetched
if self.today_revenue is not None:
Expand All @@ -273,15 +362,27 @@ async def _async_update_data(self) -> CheckwattResp: # noqa: C901
resp["spot_price"] = cw_inst.get_spot_price_excl_vat(time_hour)
resp["price_zone"] = cw_inst.price_zone

if cw_inst.meter_data is not None and use_cm10_sensor:
if cw_inst.meter_status == "offline":
resp["cm10_status"] = "Offline"
elif cw_inst.meter_under_test:
resp["cm10_status"] = "Test Pending"
else:
resp["cm10_status"] = "Active"

resp["cm10_version"] = cw_inst.meter_version

# Check if FCR-D State has changed and dispatch it ACTIVATED/ DEACTIVATED
old_state = self.fcrd_state
new_state = cw_inst.fcrd_state
new_state = fcrd_state

# During test, toggle (every minute)
if BASIC_TEST is True:
if old_state == "ACTIVATED":
new_state = "DEACTIVATE"
if old_state == "DEACTIVATE":
new_state = "FAIL ACTIVATION"
if old_state == "FAIL ACTIVATION":
new_state = "ACTIVATED"

if old_state != new_state:
Expand All @@ -290,13 +391,13 @@ async def _async_update_data(self) -> CheckwattResp: # noqa: C901
"data": {
"current_fcrd": {
"state": old_state,
"status": self.fcrd_percentage,
"info": self.fcrd_info,
"date": self.fcrd_timestamp,
},
"new_fcrd": {
"state": new_state,
"status": cw_inst.fcrd_percentage,
"date": cw_inst.fcrd_timestamp,
"info": fcrd_info,
"date": fcrd_timestamp,
},
},
}
Expand All @@ -310,8 +411,8 @@ async def _async_update_data(self) -> CheckwattResp: # noqa: C901

# Update self to discover next change
self.fcrd_state = new_state
self.fcrd_percentage = cw_inst.fcrd_percentage
self.fcrd_timestamp = cw_inst.fcrd_timestamp
self.fcrd_info = fcrd_info
self.fcrd_timestamp = fcrd_timestamp

return resp

Expand Down
6 changes: 6 additions & 0 deletions custom_components/checkwatt/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from homeassistant.exceptions import HomeAssistantError

from .const import (
CONF_CM10_SENSOR,
CONF_DETAILED_ATTRIBUTES,
CONF_DETAILED_SENSORS,
CONF_PUSH_CW_TO_RANK,
Expand Down Expand Up @@ -78,6 +79,7 @@ async def async_step_user(
CONF_DETAILED_SENSORS: False,
CONF_DETAILED_ATTRIBUTES: False,
CONF_PUSH_CW_TO_RANK: False,
CONF_CM10_SENSOR: False,
},
)

Expand Down Expand Up @@ -124,6 +126,10 @@ async def async_step_init(
CONF_PUSH_CW_TO_RANK,
default=self.config_entry.options.get(CONF_PUSH_CW_TO_RANK),
): bool,
vol.Required(
CONF_CM10_SENSOR,
default=self.config_entry.options.get(CONF_CM10_SENSOR),
): bool,
}
),
)
Expand Down
9 changes: 8 additions & 1 deletion custom_components/checkwatt/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
CONF_DETAILED_SENSORS: Final = "show_details"
CONF_DETAILED_ATTRIBUTES: Final = "show_detailed_attributes"
CONF_PUSH_CW_TO_RANK: Final = "push_to_cw_rank"
CONF_CM10_SENSOR = "cm10_sensor"

# Misc
P_UNKNOWN = "Unknown"
Expand All @@ -32,13 +33,14 @@
C_BATTERY_POWER = "battery_power"
C_CHARGE_PEAK = "charge_peak"
C_CITY = "city"
C_CM10_VERSION = "cm10_version"
C_DISCHARGE_PEAK = "discharge_peak"
C_DISPLAY_NAME = "display_name"
C_DSO = "dso"
C_GRID_POWER = "grid_power"
C_ENERGY_PROVIDER = "energy_provider"
C_FCRD_DATE = "fcr_d_date"
C_FCRD_STATE = "fcr_d_state"
C_FCRD_INFO = "fcr_d_info"
C_FCRD_STATUS = "fcr_d_status"
C_NEXT_UPDATE_TIME = "next_update"
C_PRICE_ZONE = "price_zone"
Expand All @@ -53,6 +55,11 @@
C_UPDATE_TIME = "last_update"
C_VAT = "vat"
C_ZIP = "zip_code"
C_CHARGE_PEAK_AC = "charge_peak_ac"
C_CHARGE_PEAK_DC = "charge_peak_dc"
C_DISCHARGE_PEAK_AC = "discharge_peak_ac"
C_DISCHARGE_PEAK_DC = "discharge_peak_dc"


# CheckWatt Event Signals
EVENT_SIGNAL_FCRD = "fcrd"
12 changes: 11 additions & 1 deletion custom_components/checkwatt/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

EVENT_FCRD_ACTIVATED = "fcrd_activated"
EVENT_FCRD_DEACTIVATED = "fcrd_deactivated"
EVENT_FCRD_FAILED = "fcrd_failed"

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -83,7 +84,11 @@ def device_info(self) -> DeviceInfo:
class CheckWattFCRDEvent(AbstractCheckwattEvent):
"""Representation of a CheckWatt sleep event."""

_attr_event_types = [EVENT_FCRD_ACTIVATED, EVENT_FCRD_DEACTIVATED]
_attr_event_types = [
EVENT_FCRD_ACTIVATED,
EVENT_FCRD_DEACTIVATED,
EVENT_FCRD_FAILED,
]

def __init__(
self,
Expand Down Expand Up @@ -112,6 +117,8 @@ async def async_added_to_hass(self) -> None:
event = EVENT_FCRD_ACTIVATED
elif self._coordinator.data["fcr_d_status"] == "DEACTIVATE":
event = EVENT_FCRD_DEACTIVATED
elif self._coordinator.data["fcr_d_status"] == "FAIL ACTIVATION":
event = EVENT_FCRD_FAILED

if event is not None:
self._trigger_event(event)
Expand All @@ -130,6 +137,9 @@ def handle_event(self, signal_payload) -> None:
event = EVENT_FCRD_ACTIVATED
elif signal_payload["data"]["new_fcrd"]["state"] == "DEACTIVATE":
event = EVENT_FCRD_DEACTIVATED
elif signal_payload["data"]["new_fcrd"]["state"] == "FAIL ACTIVATION":
event = EVENT_FCRD_FAILED

else:
_LOGGER.error(
"Signal %s payload did not include correct data", EVENT_SIGNAL_FCRD
Expand Down
4 changes: 2 additions & 2 deletions custom_components/checkwatt/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
"homekit": {},
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/faanskit/ha-checkwatt/issues",
"requirements": ["pycheckwatt>=0.1.9", "aiohttp>=3.9.1"],
"requirements": ["pycheckwatt>=0.1.10", "aiohttp>=3.9.1"],
"ssdp": [],
"version": "0.1.5",
"version": "0.1.7",
"zeroconf": []
}
Loading
Loading