diff --git a/custom_components/checkwatt/__init__.py b/custom_components/checkwatt/__init__.py index fc779fa..74454cd 100644 --- a/custom_components/checkwatt/__init__.py +++ b/custom_components/checkwatt/__init__.py @@ -1,11 +1,10 @@ """The CheckWatt integration.""" + from __future__ import annotations import asyncio from datetime import time, timedelta import logging -import random -import re from typing import TypedDict import aiohttp @@ -22,10 +21,11 @@ from .const import ( BASIC_TEST, CONF_CM10_SENSOR, - CONF_DETAILED_SENSORS, + CONF_CWR_NAME, + CONF_POWER_SENSORS, CONF_PUSH_CW_TO_RANK, - CONF_UPDATE_INTERVAL, - CONF_UPDATE_INTERVAL_FCRD, + CONF_UPDATE_INTERVAL_ALL, + CONF_UPDATE_INTERVAL_MONETARY, DOMAIN, EVENT_SIGNAL_FCRD, INTEGRATION_NAME, @@ -46,17 +46,28 @@ class CheckwattResp(TypedDict): zip: str city: str display_name: str - revenue: float - fees: float - battery_charge_peak: float - battery_discharge_peak: float - tomorrow_revenue: float - tomorrow_fees: float + dso: str + energy_provider: str + + battery_power: float + grid_power: float + solar_power: float + battery_soc: float + charge_peak_ac: float + charge_peak_dc: float + discharge_peak_ac: float + discharge_peak_dc: float + + today_net_revenue: float + tomorrow_net_revenue: float + monthly_net_revenue: float + annual_net_revenue: float + month_estimate: float + daily_average: float + update_time: str next_update_time: str - fcr_d_status: str - fcr_d_info: str - fcr_d_date: str + total_solar_energy: float total_charging_energy: float total_discharging_energy: float @@ -64,20 +75,12 @@ class CheckwattResp(TypedDict): total_export_energy: float spot_price: float price_zone: str - annual_revenue: float - annual_fees: float - grid_power: float - solar_power: float - battery_power: float - 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 + fcr_d_status: str + fcr_d_info: str + fcr_d_date: str async def update_listener(hass: HomeAssistant, entry): @@ -134,37 +137,6 @@ async def getPeakData(cw_inst): 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.""" @@ -174,29 +146,23 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: hass, _LOGGER, name=DOMAIN, - update_interval=timedelta(minutes=CONF_UPDATE_INTERVAL), + update_interval=timedelta(minutes=CONF_UPDATE_INTERVAL_ALL), ) self._entry = entry - self.update_monetary = 0 - self.update_time = None - self.next_update_time = None - self.today_revenue = None - self.today_fees = None - self.tomorrow_revenue = None - self.tomorrow_fees = None - self.annual_revenue = None - self.annual_fees = None - self.last_annual_update = None self.last_cw_rank_push = None self.is_boot = True self.energy_provider = None - self.random_offset = random.randint(0, 14) self.fcrd_state = 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) + self.update_all = 0 + self.fcrd_today_net_revenue = None + self.fcrd_tomorrow_net_revenue = None + self.fcrd_month_net_revenue = None + self.fcrd_month_net_estimate = None + self.fcrd_daily_net_average = None + self.fcrd_year_net_revenue = None @property def entry_id(self) -> str: @@ -209,9 +175,10 @@ async def _async_update_data(self) -> CheckwattResp: # noqa: C901 try: username = self._entry.data.get(CONF_USERNAME) password = self._entry.data.get(CONF_PASSWORD) - use_detailed_sensors = self._entry.options.get(CONF_DETAILED_SENSORS) + use_power_sensors = self._entry.options.get(CONF_POWER_SENSORS) push_to_cw_rank = self._entry.options.get(CONF_PUSH_CW_TO_RANK) use_cm10_sensor = self._entry.options.get(CONF_CM10_SENSOR) + cwr_name = self._entry.options.get(CONF_CWR_NAME) async with CheckwattManager( username, password, INTEGRATION_NAME @@ -219,29 +186,47 @@ async def _async_update_data(self) -> CheckwattResp: # noqa: C901 if not await cw_inst.login(): _LOGGER.error("Failed to login, abort update") raise UpdateFailed("Failed to login") + 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 not await cw_inst.get_energy_flow(): + _LOGGER.error("Failed to get energy flows, abort update") + raise UpdateFailed("Unknown error get_energy_flow") + 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) + # Only fetch some parameters every 15 min + if self.update_all == 0 and not self.is_boot: + self.update_all = CONF_UPDATE_INTERVAL_MONETARY + _LOGGER.debug("Fetching daily revenue") + if not await cw_inst.get_fcrd_today_net_revenue(): + raise UpdateFailed("Unknown error get_fcrd_revenue") + + _LOGGER.debug("Fetching monthly revenue") + if not await cw_inst.get_fcrd_month_net_revenue(): + raise UpdateFailed("Unknown error get_revenue_month") + + _LOGGER.debug("Fetching annual revenue") + if not await cw_inst.get_fcrd_year_net_revenue(): + raise UpdateFailed("Unknown error get_revenue_year") + + self.fcrd_today_net_revenue = cw_inst.fcrd_today_net_revenue + self.fcrd_tomorrow_net_revenue = cw_inst.fcrd_tomorrow_net_revenue + self.fcrd_month_net_revenue = cw_inst.fcrd_month_net_revenue + self.fcrd_month_net_estimate = cw_inst.fcrd_month_net_estimate + self.fcrd_daily_net_average = cw_inst.fcrd_daily_net_average + self.fcrd_year_net_revenue = cw_inst.fcrd_year_net_revenue + + if not self.is_boot: + self.update_all -= 1 - # Prevent slow funcion to be called at boot. - # The revenue sensors will be updated after ca 1 min - self.update_no += 1 - if self.update_no > 2: - self.is_boot = False if self.is_boot: + self.is_boot = False if ( "Meter" in cw_inst.customer_details and len(cw_inst.customer_details["Meter"]) > 0 @@ -252,45 +237,14 @@ async def _async_update_data(self) -> CheckwattResp: # noqa: C901 ) # Store fcrd_state at boot, used to spark event - self.fcrd_state = fcrd_state + self.fcrd_state = cw_inst.fcrd_state self._id = cw_inst.customer_details["Id"] - else: - if self.update_monetary == 0: - _LOGGER.debug("Fetching FCR-D data from CheckWatt") - self.update_time = dt_util.now().strftime("%Y-%m-%d %H:%M:%S") - end_date = dt_util.now() + timedelta( - minutes=CONF_UPDATE_INTERVAL_FCRD - ) - self.next_update_time = end_date.strftime("%Y-%m-%d %H:%M:%S") - self.update_monetary = CONF_UPDATE_INTERVAL_FCRD - if not await cw_inst.get_fcrd_revenue(): - raise UpdateFailed("Unknown error get_fcrd_revenue") - self.today_revenue, self.today_fees = cw_inst.today_revenue - ( - self.tomorrow_revenue, - self.tomorrow_fees, - ) = cw_inst.tomorrow_revenue - - if self.last_annual_update is None or ( - dt_util.now().time() - >= time(3, self.random_offset) # Wait until 3am +- 15 min - and dt_util.start_of_local_day(dt_util.now()) - != dt_util.start_of_local_day(self.last_annual_update) - ): - _LOGGER.debug("Fetching annual revenue") - if not await cw_inst.get_fcrd_revenueyear(): - raise UpdateFailed("Unknown error get_fcrd_revenueyear") - self.annual_revenue, self.annual_fees = cw_inst.year_revenue - self.last_annual_update = dt_util.now() - - self.update_monetary -= 1 - # Price Zone is used both as Detailed Sensor and by Push to CheckWattRank - if push_to_cw_rank or use_detailed_sensors: + if push_to_cw_rank or use_power_sensors: if not await cw_inst.get_price_zone(): raise UpdateFailed("Unknown error get_price_zone") - if use_detailed_sensors: + if use_power_sensors: if not await cw_inst.get_power_data(): raise UpdateFailed("Unknown error get_power_data") if not await cw_inst.get_spot_price(): @@ -304,7 +258,7 @@ async def _async_update_data(self) -> CheckwattResp: # noqa: C901 != dt_util.start_of_local_day(self.last_cw_rank_push) ): _LOGGER.debug("Pushing to CheckWattRank") - if await self.push_to_checkwatt_rank(cw_inst): + if await self.push_to_checkwatt_rank(cw_inst, cwr_name): self.last_cw_rank_push = dt_util.now() resp: CheckwattResp = { @@ -315,13 +269,6 @@ async def _async_update_data(self) -> CheckwattResp: # noqa: C901 "zip": cw_inst.customer_details["ZipCode"], "city": cw_inst.customer_details["City"], "display_name": cw_inst.customer_details["Meter"][0]["DisplayName"], - "update_time": self.update_time, - "next_update_time": self.next_update_time, - "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"], "energy_provider": self.energy_provider, } @@ -342,24 +289,33 @@ async def _async_update_data(self) -> CheckwattResp: # noqa: C901 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: - resp["revenue"] = self.today_revenue - resp["fees"] = self.today_fees - resp["tomorrow_revenue"] = self.tomorrow_revenue - resp["tomorrow_fees"] = self.tomorrow_fees - - if self.annual_revenue is not None: - resp["annual_revenue"] = self.annual_revenue - resp["annual_fees"] = self.annual_fees - - if use_detailed_sensors: + if self.fcrd_today_net_revenue is not None: + resp["today_net_revenue"] = self.fcrd_today_net_revenue + resp["tomorrow_net_revenue"] = self.fcrd_tomorrow_net_revenue + if self.fcrd_month_net_revenue is not None: + resp["monthly_net_revenue"] = self.fcrd_month_net_revenue + resp["month_estimate"] = self.fcrd_month_net_estimate + resp["daily_average"] = self.fcrd_daily_net_average + if self.fcrd_year_net_revenue is not None: + resp["annual_net_revenue"] = self.fcrd_year_net_revenue + + update_time = dt_util.now().strftime("%Y-%m-%d %H:%M:%S") + next_update = dt_util.now() + timedelta( + minutes=CONF_UPDATE_INTERVAL_MONETARY + ) + next_update_time = next_update.strftime("%Y-%m-%d %H:%M:%S") + resp["update_time"] = update_time + resp["next_update_time"] = next_update_time + + if use_power_sensors: resp["total_solar_energy"] = cw_inst.total_solar_energy resp["total_charging_energy"] = cw_inst.total_charging_energy resp["total_discharging_energy"] = cw_inst.total_discharging_energy resp["total_import_energy"] = cw_inst.total_import_energy resp["total_export_energy"] = cw_inst.total_export_energy - time_hour = int(dt_util.now().strftime("%H")) - resp["spot_price"] = cw_inst.get_spot_price_excl_vat(time_hour) + resp["spot_price"] = cw_inst.get_spot_price_excl_vat( + int(dt_util.now().strftime("%H")) + ) resp["price_zone"] = cw_inst.price_zone if cw_inst.meter_data is not None and use_cm10_sensor: @@ -371,10 +327,13 @@ async def _async_update_data(self) -> CheckwattResp: # noqa: C901 resp["cm10_status"] = "Active" resp["cm10_version"] = cw_inst.meter_version + resp["fcr_d_status"] = cw_inst.fcrd_state + resp["fcr_d_info"] = cw_inst.fcrd_info + resp["fcr_d_date"] = cw_inst.fcrd_timestamp # Check if FCR-D State has changed and dispatch it ACTIVATED/ DEACTIVATED old_state = self.fcrd_state - new_state = fcrd_state + new_state = cw_inst.fcrd_state # During test, toggle (every minute) if BASIC_TEST is True: @@ -396,8 +355,8 @@ async def _async_update_data(self) -> CheckwattResp: # noqa: C901 }, "new_fcrd": { "state": new_state, - "info": fcrd_info, - "date": fcrd_timestamp, + "info": cw_inst.fcrd_info, + "date": cw_inst.fcrd_timestamp, }, }, } @@ -411,8 +370,8 @@ async def _async_update_data(self) -> CheckwattResp: # noqa: C901 # Update self to discover next change self.fcrd_state = new_state - self.fcrd_info = fcrd_info - self.fcrd_timestamp = fcrd_timestamp + self.fcrd_info = cw_inst.fcrd_info + self.fcrd_timestamp = cw_inst.fcrd_timestamp return resp @@ -421,9 +380,9 @@ async def _async_update_data(self) -> CheckwattResp: # noqa: C901 except CheckwattError as err: raise UpdateFailed(str(err)) from err - async def push_to_checkwatt_rank(self, cw_inst): + async def push_to_checkwatt_rank(self, cw_inst, cwr_name): """Push data to CheckWattRank.""" - if self.today_revenue is not None: + if self.today_net_revenue is not None: if ( "Meter" in cw_inst.customer_details and len(cw_inst.customer_details["Meter"]) > 0 @@ -437,13 +396,15 @@ async def push_to_checkwatt_rank(self, cw_inst): "electricity_company": self.energy_provider, "electricity_area": cw_inst.price_zone, "installed_power": cw_inst.battery_charge_peak, - "today_gross_income": self.today_revenue, - "today_fee": self.today_fees, - "today_net_income": self.today_revenue - self.today_fees, + "today_gross_income": 0, + "today_fee": 0, + "today_net_income": self.today_net_revenue, "reseller_id": cw_inst.customer_details["Meter"][0]["ResellerId"], } if BASIC_TEST: payload["display_name"] = "xxTESTxx" + elif cwr_name != "": + payload["display_name"] = cwr_name else: payload["display_name"] = cw_inst.customer_details["Meter"][0][ "DisplayName" diff --git a/custom_components/checkwatt/config_flow.py b/custom_components/checkwatt/config_flow.py index 9ecaa22..5141711 100644 --- a/custom_components/checkwatt/config_flow.py +++ b/custom_components/checkwatt/config_flow.py @@ -1,4 +1,5 @@ """Config flow for CheckWatt integration.""" + from __future__ import annotations import logging @@ -15,8 +16,8 @@ from .const import ( CONF_CM10_SENSOR, - CONF_DETAILED_ATTRIBUTES, - CONF_DETAILED_SENSORS, + CONF_CWR_NAME, + CONF_POWER_SENSORS, CONF_PUSH_CW_TO_RANK, DOMAIN, ) @@ -76,10 +77,10 @@ async def async_step_user( title=CONF_TITLE, data=self.data, options={ - CONF_DETAILED_SENSORS: False, - CONF_DETAILED_ATTRIBUTES: False, + CONF_POWER_SENSORS: False, CONF_PUSH_CW_TO_RANK: False, - CONF_CM10_SENSOR: False, + CONF_CM10_SENSOR: True, + CONF_CWR_NAME: "", }, ) @@ -115,12 +116,8 @@ async def async_step_init( data_schema=vol.Schema( { vol.Required( - CONF_DETAILED_SENSORS, - default=self.config_entry.options.get(CONF_DETAILED_SENSORS), - ): bool, - vol.Required( - CONF_DETAILED_ATTRIBUTES, - default=self.config_entry.options.get(CONF_DETAILED_ATTRIBUTES), + CONF_POWER_SENSORS, + default=self.config_entry.options.get(CONF_POWER_SENSORS), ): bool, vol.Required( CONF_PUSH_CW_TO_RANK, @@ -130,6 +127,10 @@ async def async_step_init( CONF_CM10_SENSOR, default=self.config_entry.options.get(CONF_CM10_SENSOR), ): bool, + vol.Optional( + CONF_CWR_NAME, + default=self.config_entry.options.get(CONF_CWR_NAME), + ): str, } ), ) diff --git a/custom_components/checkwatt/const.py b/custom_components/checkwatt/const.py index 225a5f4..7a50588 100644 --- a/custom_components/checkwatt/const.py +++ b/custom_components/checkwatt/const.py @@ -1,21 +1,21 @@ """Constants for the CheckWatt integration.""" + from typing import Final DOMAIN = "checkwatt" INTEGRATION_NAME = "ha-checkwatt" # Update interval for regular sensors is once every minute -# For FCR-D since it is slow and resource consuming, is set to once per 15 minute -CONF_UPDATE_INTERVAL = 1 -CONF_UPDATE_INTERVAL_FCRD = 15 +CONF_UPDATE_INTERVAL_ALL = 1 +CONF_UPDATE_INTERVAL_MONETARY = 15 ATTRIBUTION = "Data provided by CheckWatt EnergyInBalance" MANUFACTURER = "CheckWatt" CHECKWATT_MODEL = "CheckWatt" -CONF_DETAILED_SENSORS: Final = "show_details" -CONF_DETAILED_ATTRIBUTES: Final = "show_detailed_attributes" +CONF_POWER_SENSORS: Final = "show_details" CONF_PUSH_CW_TO_RANK: Final = "push_to_cw_rank" -CONF_CM10_SENSOR = "cm10_sensor" +CONF_CM10_SENSOR: Final = "cm10_sensor" +CONF_CWR_NAME: Final = "cwr_name" # Misc P_UNKNOWN = "Unknown" @@ -27,14 +27,10 @@ # NOTE Keep these names aligned with strings.json # C_ADR = "street_address" -C_ANNUAL_FEES = "annual_fees" -C_ANNUAL_FEE_RATE = "annual_fee_rate" -C_ANNUAL_GROSS = "annual_gross_income" C_BATTERY_POWER = "battery_power" -C_CHARGE_PEAK = "charge_peak" C_CITY = "city" C_CM10_VERSION = "cm10_version" -C_DISCHARGE_PEAK = "discharge_peak" +C_DAILY_AVERAGE = "daily_average" C_DISPLAY_NAME = "display_name" C_DSO = "dso" C_GRID_POWER = "grid_power" @@ -42,15 +38,10 @@ C_FCRD_DATE = "fcr_d_date" C_FCRD_INFO = "fcr_d_info" C_FCRD_STATUS = "fcr_d_status" +C_MONTH_ESITIMATE = "month_estimate" C_NEXT_UPDATE_TIME = "next_update" C_PRICE_ZONE = "price_zone" C_SOLAR_POWER = "solar_power" -C_TODAY_FEES = "today_fees" -C_TODAY_FEE_RATE = "today_fees_rate" -C_TODAY_GROSS = "today_gross_income" -C_TOMORROW_FEES = "tomorrow_fees" -C_TOMORROW_FEE_RATE = "tomorrow_fee_rate" -C_TOMORROW_GROSS = "tomorrow_gross_income" C_TOMORROW_NET = "tomorrow_net_income" C_UPDATE_TIME = "last_update" C_VAT = "vat" @@ -60,6 +51,5 @@ C_DISCHARGE_PEAK_AC = "discharge_peak_ac" C_DISCHARGE_PEAK_DC = "discharge_peak_dc" - # CheckWatt Event Signals EVENT_SIGNAL_FCRD = "fcrd" diff --git a/custom_components/checkwatt/event.py b/custom_components/checkwatt/event.py index 3674f64..2654d05 100644 --- a/custom_components/checkwatt/event.py +++ b/custom_components/checkwatt/event.py @@ -1,4 +1,5 @@ -"""Events for Withings.""" +"""Events for CheckWatt.""" + from __future__ import annotations import logging diff --git a/custom_components/checkwatt/manifest.json b/custom_components/checkwatt/manifest.json index 3206055..e57d01c 100644 --- a/custom_components/checkwatt/manifest.json +++ b/custom_components/checkwatt/manifest.json @@ -12,8 +12,8 @@ "homekit": {}, "iot_class": "cloud_polling", "issue_tracker": "https://github.com/faanskit/ha-checkwatt/issues", - "requirements": ["pycheckwatt>=0.1.10", "aiohttp>=3.9.1"], + "requirements": ["pycheckwatt>=0.2.0", "aiohttp>=3.9.1"], "ssdp": [], - "version": "0.1.7", + "version": "0.2.0", "zeroconf": [] } diff --git a/custom_components/checkwatt/sensor.py b/custom_components/checkwatt/sensor.py index 1bc462d..011afa8 100644 --- a/custom_components/checkwatt/sensor.py +++ b/custom_components/checkwatt/sensor.py @@ -1,4 +1,5 @@ """Support for CheckWatt sensors.""" + from __future__ import annotations from datetime import timedelta @@ -21,16 +22,12 @@ from .const import ( ATTRIBUTION, C_ADR, - C_ANNUAL_FEE_RATE, - C_ANNUAL_FEES, - C_ANNUAL_GROSS, C_BATTERY_POWER, - C_CHARGE_PEAK, C_CHARGE_PEAK_AC, C_CHARGE_PEAK_DC, C_CITY, C_CM10_VERSION, - C_DISCHARGE_PEAK, + C_DAILY_AVERAGE, C_DISCHARGE_PEAK_AC, C_DISCHARGE_PEAK_DC, C_DISPLAY_NAME, @@ -40,23 +37,17 @@ C_FCRD_INFO, C_FCRD_STATUS, C_GRID_POWER, + C_MONTH_ESITIMATE, C_NEXT_UPDATE_TIME, C_PRICE_ZONE, C_SOLAR_POWER, - C_TODAY_FEE_RATE, - C_TODAY_FEES, - C_TODAY_GROSS, - C_TOMORROW_FEE_RATE, - C_TOMORROW_FEES, - C_TOMORROW_GROSS, C_TOMORROW_NET, C_UPDATE_TIME, C_VAT, C_ZIP, CHECKWATT_MODEL, CONF_CM10_SENSOR, - CONF_DETAILED_ATTRIBUTES, - CONF_DETAILED_SENSORS, + CONF_POWER_SENSORS, DOMAIN, MANUFACTURER, ) @@ -75,6 +66,15 @@ state_class=SensorStateClass.TOTAL, translation_key="daily_yield_sensor", ), + "monthly": SensorEntityDescription( + key="monthly_yield", + name="CheckWatt Monthly Yield", + icon="mdi:account-cash-outline", + device_class=SensorDeviceClass.MONETARY, + native_unit_of_measurement="SEK", + state_class=SensorStateClass.TOTAL, + translation_key="monthly_yield_sensor", + ), "annual": SensorEntityDescription( key="annual_yield", name="CheckWatt Annual Yield", @@ -179,26 +179,23 @@ async def async_setup_entry( coordinator: CheckwattCoordinator = hass.data[DOMAIN][entry.entry_id] entities: list[AbstractCheckwattSensor] = [] checkwatt_data: CheckwattResp = coordinator.data - use_detailed_sensors = entry.options.get(CONF_DETAILED_SENSORS) - use_detailed_attributes = entry.options.get(CONF_DETAILED_ATTRIBUTES) + use_power_sensors = entry.options.get(CONF_POWER_SENSORS) use_cm10_sensor = entry.options.get(CONF_CM10_SENSOR) _LOGGER.debug("Setting up CheckWatt sensor for %s", checkwatt_data["display_name"]) for key, description in CHECKWATT_MONETARY_SENSORS.items(): if key == "daily": - entities.append( - CheckwattSensor(coordinator, description, use_detailed_attributes) - ) + entities.append(CheckwattSensor(coordinator, description)) + elif key == "monthly": + entities.append(CheckwattMonthlySensor(coordinator, description)) elif key == "annual": - entities.append( - CheckwattAnnualSensor(coordinator, description, use_detailed_attributes) - ) + entities.append(CheckwattAnnualSensor(coordinator, description)) elif key == "battery": entities.append(CheckwattBatterySoCSensor(coordinator, description)) elif key == "cm10" and use_cm10_sensor: entities.append(CheckwattCM10Sensor(coordinator, description)) - if use_detailed_sensors: + if use_power_sensors: _LOGGER.debug( "Setting up detailed CheckWatt sensors for %s", checkwatt_data["display_name"], @@ -223,6 +220,7 @@ def __init__( description: SensorEntityDescription, ) -> None: """Initialize the sensor.""" + _LOGGER.debug("Creating %s sensor", description.name) super().__init__(coordinator) self._coordinator = coordinator self._device_model = CHECKWATT_MODEL @@ -253,11 +251,9 @@ def __init__( self, coordinator: CheckwattCoordinator, description: SensorEntityDescription, - use_detailed_attributes, ) -> None: """Initialize the sensor.""" super().__init__(coordinator=coordinator, description=description) - self.use_detailed_attributes = use_detailed_attributes self._attr_unique_id = f'checkwattUid_{self._coordinator.data["id"]}' self._attr_extra_state_attributes = {} @@ -285,60 +281,22 @@ def __init__( self._attr_extra_state_attributes.update( {C_ENERGY_PROVIDER: self._coordinator.data["energy_provider"]} ) - if "revenue" in self._coordinator.data and "fees" in self._coordinator.data: - revenue = self._coordinator.data["revenue"] - fees = self._coordinator.data["fees"] - if self.use_detailed_attributes: # Only show these at detailed attribues - self._attr_extra_state_attributes[C_TODAY_GROSS] = round(revenue, 2) - self._attr_extra_state_attributes[C_TODAY_FEES] = round(fees, 2) - if revenue > 0: - self._attr_extra_state_attributes[ - C_TODAY_FEE_RATE - ] = f"{round((fees / revenue) * 100, 2)} %" - else: - self._attr_extra_state_attributes[C_TODAY_FEE_RATE] = "N/A %" - - if ( - "tomorrow_revenue" in self._coordinator.data - and "tomorrow_fees" in self._coordinator.data - ): - tomorrow_revenue = self._coordinator.data["tomorrow_revenue"] - tomorrow_fees = self._coordinator.data["tomorrow_fees"] - self._attr_extra_state_attributes[C_TOMORROW_NET] = round( - (tomorrow_revenue - tomorrow_fees), 2 - ) - if self.use_detailed_attributes: # Only show these at detailed attribues - self._attr_extra_state_attributes[C_TOMORROW_GROSS] = round( - tomorrow_revenue, 2 - ) - self._attr_extra_state_attributes[C_TOMORROW_FEES] = round( - tomorrow_fees, 2 - ) - if tomorrow_revenue > 0: - self._attr_extra_state_attributes[ - C_TOMORROW_FEE_RATE - ] = f"{round((tomorrow_fees / tomorrow_revenue) * 100, 2 )} %" - else: - self._attr_extra_state_attributes[C_TOMORROW_FEE_RATE] = "N/A %" - - if use_detailed_attributes: - # Add extra attributes as required - if "update_time" in self._coordinator.data: - self._attr_extra_state_attributes.update( - {C_UPDATE_TIME: self._coordinator.data["update_time"]} - ) - if "next_update_time" in self._coordinator.data: - self._attr_extra_state_attributes.update( - {C_NEXT_UPDATE_TIME: self._coordinator.data["next_update_time"]} - ) - if "battery_charge_peak" in self._coordinator.data: - self._attr_extra_state_attributes.update( - {C_CHARGE_PEAK: self._coordinator.data["battery_charge_peak"]} - ) - if "battery_discharge_peak" in self._coordinator.data: - self._attr_extra_state_attributes.update( - {C_DISCHARGE_PEAK: self._coordinator.data["battery_discharge_peak"]} - ) + if "tomorrow_net_revenue" in self._coordinator.data: + self._attr_extra_state_attributes.update( + { + C_TOMORROW_NET: round( + self._coordinator.data["tomorrow_net_revenue"], 2 + ) + } + ) + if "update_time" in self._coordinator.data: + self._attr_extra_state_attributes.update( + {C_UPDATE_TIME: self._coordinator.data["update_time"]} + ) + if "next_update_time" in self._coordinator.data: + self._attr_extra_state_attributes.update( + {C_NEXT_UPDATE_TIME: self._coordinator.data["next_update_time"]} + ) self._attr_available = False @@ -350,202 +308,111 @@ async def async_update(self) -> None: def _handle_coordinator_update(self) -> None: """Get the latest data and updates the states.""" # Update the native value - if "revenue" in self._coordinator.data and "fees" in self._coordinator.data: - revenue = self._coordinator.data["revenue"] - fees = self._coordinator.data["fees"] - self._attr_native_value = round((revenue - fees), 2) - if self.use_detailed_attributes: # Only show these at detailed attribues - self._attr_extra_state_attributes[C_TODAY_GROSS] = round(revenue, 2) - self._attr_extra_state_attributes[C_TODAY_FEES] = round(fees, 2) - if revenue > 0: - self._attr_extra_state_attributes[ - C_TODAY_FEE_RATE - ] = f"{round((fees / revenue) * 100, 2)} %" - else: - self._attr_extra_state_attributes[C_TODAY_FEE_RATE] = "N/A %" - - # Update the normal attributes - if ( - "tomorrow_revenue" in self._coordinator.data - and "tomorrow_fees" in self._coordinator.data - ): - tomorrow_revenue = self._coordinator.data["tomorrow_revenue"] - tomorrow_fees = self._coordinator.data["tomorrow_fees"] - self._attr_extra_state_attributes[C_TOMORROW_NET] = round( - (tomorrow_revenue - tomorrow_fees), 2 - ) - if self.use_detailed_attributes: # Only show these at detailed attribues - self._attr_extra_state_attributes[C_TOMORROW_GROSS] = round( - tomorrow_revenue, 2 - ) - self._attr_extra_state_attributes[C_TOMORROW_FEES] = round( - tomorrow_fees, 2 - ) - if tomorrow_revenue > 0: - self._attr_extra_state_attributes[ - C_TOMORROW_FEE_RATE - ] = f"{round((tomorrow_fees / tomorrow_revenue) * 100, 2)} %" - else: - self._attr_extra_state_attributes[C_TOMORROW_FEE_RATE] = "N/A %" - - # Update the extra attributes - if self.use_detailed_attributes: - if "update_time" in self._coordinator.data: - self._attr_extra_state_attributes.update( - {C_UPDATE_TIME: self._coordinator.data["update_time"]} - ) - if "next_update_time" in self._coordinator.data: - self._attr_extra_state_attributes.update( - {C_NEXT_UPDATE_TIME: self._coordinator.data["next_update_time"]} - ) - if "battery_charge_peak" in self._coordinator.data: - self._attr_extra_state_attributes.update( - {C_CHARGE_PEAK: self._coordinator.data["battery_charge_peak"]} - ) - if "battery_discharge_peak" in self._coordinator.data: - self._attr_extra_state_attributes.update( - {C_DISCHARGE_PEAK: self._coordinator.data["battery_discharge_peak"]} - ) + if "tomorrow_net_revenue" in self._coordinator.data: + self._attr_extra_state_attributes.update( + { + C_TOMORROW_NET: round( + self._coordinator.data["tomorrow_net_revenue"], 2 + ) + } + ) + if "update_time" in self._coordinator.data: + self._attr_extra_state_attributes.update( + {C_UPDATE_TIME: self._coordinator.data["update_time"]} + ) + if "next_update_time" in self._coordinator.data: + self._attr_extra_state_attributes.update( + {C_NEXT_UPDATE_TIME: self._coordinator.data["next_update_time"]} + ) super()._handle_coordinator_update() @property def native_value(self) -> str | None: """Get the latest state value.""" - if "revenue" in self._coordinator.data and "fees" in self._coordinator.data: - return round( - self._coordinator.data["revenue"] - self._coordinator.data["fees"], 2 - ) + if "today_net_revenue" in self._coordinator.data: + return round(self._coordinator.data["today_net_revenue"], 2) return None -class CheckwattAnnualSensor(AbstractCheckwattSensor): - """Representation of a CheckWatt Annual Revenue sensor.""" +class CheckwattMonthlySensor(AbstractCheckwattSensor): + """Representation of a CheckWatt Monthly Revenue sensor.""" def __init__( self, coordinator: CheckwattCoordinator, description: SensorEntityDescription, - use_detailed_attributes, ) -> None: """Initialize the sensor.""" super().__init__(coordinator=coordinator, description=description) - self.use_detailed_attributes = use_detailed_attributes - self._attr_unique_id = f'checkwattUid_Annual_{self._coordinator.data["id"]}' - self.total_annual_revenue = None - self.total_annual_fee = None - + self._attr_unique_id = f'checkwattUid_Monthly_{self._coordinator.data["id"]}' self._attr_extra_state_attributes = {} - if "address" in self._coordinator.data: + self._attr_available = False + + async def async_update(self) -> None: + """Get the latest data and updates the states.""" + if "month_estimate" in self._coordinator.data: self._attr_extra_state_attributes.update( - {C_ADR: self._coordinator.data["address"]} + {C_MONTH_ESITIMATE: round(self._coordinator.data["month_estimate"], 2)} ) - if "zip" in self._coordinator.data: + if "daily_average" in self._coordinator.data: self._attr_extra_state_attributes.update( - {C_ZIP: self._coordinator.data["zip"]} + {C_DAILY_AVERAGE: round(self._coordinator.data["daily_average"], 2)} ) - if "city" in self._coordinator.data: + self._attr_available = True + + @callback + def _handle_coordinator_update(self) -> None: + """Get the latest data and updates the states.""" + self._attr_native_value = round( + self._coordinator.data["monthly_net_revenue"], 2 + ) + if "month_estimate" in self._coordinator.data: self._attr_extra_state_attributes.update( - {C_CITY: self._coordinator.data["city"]} + {C_MONTH_ESITIMATE: round(self._coordinator.data["month_estimate"], 2)} ) - if ( - "annual_revenue" in self._coordinator.data - and "annual_fees" in self._coordinator.data - and "revenue" in self._coordinator.data - and "fees" in self._coordinator.data - and "tomorrow_revenue" in self._coordinator.data - and "tomorrow_fees" in self._coordinator.data - ): - # Annual revenue does not contain today and tomorrow revenue - # and is only fetched once daily. - # To obtain Total Annual revenue, it needs to be calculated - annual_revenue = self._coordinator.data["annual_revenue"] - annual_fees = self._coordinator.data["annual_fees"] - today_revenue = self._coordinator.data["revenue"] - today_fees = self._coordinator.data["fees"] - tomorrow_revenue = self._coordinator.data["tomorrow_revenue"] - tomorrow_fees = self._coordinator.data["tomorrow_fees"] - self.total_annual_revenue = ( - annual_revenue + today_revenue + tomorrow_revenue - ) - self.total_annual_fee = annual_fees + today_fees + tomorrow_fees - self._attr_native_value = round( - (self.total_annual_revenue - self.total_annual_fee), 2 - ) - - if self.use_detailed_attributes: # Only show these at detailed attribues - self._attr_extra_state_attributes[C_ANNUAL_GROSS] = round( - self.total_annual_revenue, 2 - ) - self._attr_extra_state_attributes[C_ANNUAL_FEES] = round( - self.total_annual_fee, 2 - ) - if self.total_annual_revenue > 0: - self._attr_extra_state_attributes[ - C_ANNUAL_FEE_RATE - ] = f"{round((self.total_annual_fee / self.total_annual_revenue) * 100, 2)} %" - else: - self._attr_extra_state_attributes[C_ANNUAL_FEE_RATE] = "N/A %" + if "daily_average" in self._coordinator.data: + self._attr_extra_state_attributes.update( + {C_DAILY_AVERAGE: round(self._coordinator.data["daily_average"], 2)} + ) + super()._handle_coordinator_update() + + @property + def native_value(self) -> str | None: + """Get the latest state value.""" + if "monthly_net_revenue" in self._coordinator.data: + return round(self._coordinator.data["monthly_net_revenue"], 2) + return None + + +class CheckwattAnnualSensor(AbstractCheckwattSensor): + """Representation of a CheckWatt Annual Revenue sensor.""" + + def __init__( + self, + coordinator: CheckwattCoordinator, + description: SensorEntityDescription, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator=coordinator, description=description) + self._attr_unique_id = f'checkwattUid_Annual_{self._coordinator.data["id"]}' + self._attr_extra_state_attributes = {} self._attr_available = False async def async_update(self) -> None: """Get the latest data and updates the states.""" - self._attr_available = True + self._attr_available = False @callback def _handle_coordinator_update(self) -> None: """Get the latest data and updates the states.""" - # Update the native value - if ( - "annual_revenue" in self._coordinator.data - and "annual_fees" in self._coordinator.data - and "revenue" in self._coordinator.data - and "fees" in self._coordinator.data - and "tomorrow_revenue" in self._coordinator.data - and "tomorrow_fees" in self._coordinator.data - ): - # Annual revenue does not contain today and tomorrow revenue - # and is only fetched once daily. - # To obtain Total Annual revenue, it needs to be calculated - annual_revenue = self._coordinator.data["annual_revenue"] - annual_fees = self._coordinator.data["annual_fees"] - today_revenue = self._coordinator.data["revenue"] - today_fees = self._coordinator.data["fees"] - tomorrow_revenue = self._coordinator.data["tomorrow_revenue"] - tomorrow_fees = self._coordinator.data["tomorrow_fees"] - - self.total_annual_revenue = ( - annual_revenue + today_revenue + tomorrow_revenue - ) - - self.total_annual_fee = annual_fees + today_fees + tomorrow_fees - - self._attr_native_value = round( - (self.total_annual_revenue - self.total_annual_fee), 2 - ) - if self.use_detailed_attributes: # Only show these at detailed attribues - self._attr_extra_state_attributes[C_ANNUAL_GROSS] = round( - self.total_annual_revenue, 2 - ) - self._attr_extra_state_attributes[C_ANNUAL_FEES] = round( - self.total_annual_fee, 2 - ) - if self.total_annual_revenue > 0: - self._attr_extra_state_attributes[ - C_ANNUAL_FEE_RATE - ] = f"{round((self.total_annual_fee / self.total_annual_revenue) * 100, 2)} %" - else: - self._attr_extra_state_attributes[C_ANNUAL_FEE_RATE] = "N/A %" - + self._attr_native_value = round(self._coordinator.data["annual_net_revenue"], 2) super()._handle_coordinator_update() @property def native_value(self) -> str | None: """Get the latest state value.""" - if self.total_annual_revenue is not None and self.total_annual_fee is not None: - return round( - self.total_annual_revenue - self.total_annual_fee, - 2, - ) + if "annual_net_revenue" in self._coordinator.data: + return round(self._coordinator.data["annual_net_revenue"], 2) return None @@ -559,7 +426,6 @@ def __init__( data_key, ) -> None: """Initialize the sensor.""" - _LOGGER.debug("Creating %s sensor", description.name) super().__init__(coordinator=coordinator, description=description) self.data_key = data_key @@ -589,7 +455,6 @@ def __init__( vat_key, ) -> None: """Initialize the sensor.""" - _LOGGER.debug("Creating %s sensor", description.name) super().__init__(coordinator=coordinator, description=description) self.vat_key = vat_key @@ -631,7 +496,6 @@ def __init__( description: SensorEntityDescription, ) -> None: """Initialize the sensor.""" - _LOGGER.debug("Creating %s sensor", description.name) super().__init__(coordinator=coordinator, description=description) async def async_update(self) -> None: @@ -716,7 +580,6 @@ def __init__( description: SensorEntityDescription, ) -> None: """Initialize the sensor.""" - _LOGGER.debug("Creating %s sensor", description.name) super().__init__(coordinator=coordinator, description=description) async def async_update(self) -> None: diff --git a/custom_components/checkwatt/strings.json b/custom_components/checkwatt/strings.json index 62b00fc..a26b73c 100644 --- a/custom_components/checkwatt/strings.json +++ b/custom_components/checkwatt/strings.json @@ -24,9 +24,9 @@ "init": { "data": { "show_details": "Provide energy sensors", - "show_detailed_attributes": "Offer detailed attributes", "push_to_cw_rank": "Push data to CheckWattRank", - "cm10_sensor": "Provide CM10 sensor" + "cm10_sensor": "Provide CM10 sensor", + "cwr_name": "System name for CheckWattRank" }, "description": "Select options", "title": "CheckWatt" @@ -56,64 +56,32 @@ "energy_provider": { "name": "Energy Provider" }, - "today_gross_income": { - "name": "Today Gross Income" - }, - "today_fees": { - "name": "Today Fees" - }, - "today_fees_rate": { - "name": "Today Fee Rate" - }, "tomorrow_net_income": { "name": "Tomorrow Net Income" }, - "tomorrow_gross_income": { - "name": "Tomorrow Gross Income" - }, - "tomorrow_fees": { - "name": "Tomorrow Fees" - }, - "tomorrow_fee_rate": { - "name": "Tomorrow Fee Rate" - }, "last_update": { "name": "Last update" }, "next_update": { "name": "Next update" - }, - "charge_peak": { - "name": "Charge Peak" - }, - "discharge_peak": { - "name": "Discharge Peak" } } }, - "annual_yield_sensor": { - "name": "CheckWatt Annual Net Income", + "monthly_yield_sensor": { + "name": "CheckWatt Monthly Net Income", "state_attributes": { - "street_address": { - "name": "Street Address" + "month_estimate": { + "name": "Month Estimate" }, - "zip_code": { - "name": "Zip Code" - }, - "city": { - "name": "City" - }, - "annual_gross_income": { - "name": "Annual Gross Income" - }, - "annual_fees": { - "name": "Annual Fees" - }, - "annual_fee_rate": { - "name": "Annual Fee Rate" + "daily_average": { + "name": "Daily Average" } } }, + "annual_yield_sensor": { + "name": "CheckWatt Annual Net Income", + "state_attributes": {} + }, "solar_sensor": { "name": "Solar Energy" }, diff --git a/custom_components/checkwatt/translations/en.json b/custom_components/checkwatt/translations/en.json index b2702d1..e1687ae 100644 --- a/custom_components/checkwatt/translations/en.json +++ b/custom_components/checkwatt/translations/en.json @@ -24,9 +24,9 @@ "init": { "data": { "show_details": "Provide energy sensors", - "show_detailed_attributes": "Offer detailed attributes", "push_to_cw_rank": "Push data to CheckWattRank", - "cm10_sensor": "Provide CM10 sensor" + "cm10_sensor": "Provide CM10 sensor", + "cwr_name": "System name for CheckWattRank" }, "description": "Select options", "title": "CheckWatt" @@ -56,64 +56,32 @@ "energy_provider": { "name": "Energy Provider" }, - "today_gross_income": { - "name": "Today Gross Income" - }, - "today_fees": { - "name": "Today Fees" - }, - "today_fees_rate": { - "name": "Today Fee Rate" - }, "tomorrow_net_income": { "name": "Tomorrow Net Income" }, - "tomorrow_gross_income": { - "name": "Tomorrow Gross Income" - }, - "tomorrow_fees": { - "name": "Tomorrow Fees" - }, - "tomorrow_fee_rate": { - "name": "Tomorrow Fee Rate" - }, "last_update": { "name": "Last update" }, "next_update": { "name": "Next update" - }, - "charge_peak": { - "name": "Charge Peak" - }, - "discharge_peak": { - "name": "Discharge Peak" } } }, - "annual_yield_sensor": { - "name": "CheckWatt Annual Net Income", + "monthly_yield_sensor": { + "name": "CheckWatt Monthly Net Income", "state_attributes": { - "street_address": { - "name": "Street Address" + "month_estimate": { + "name": "Month Estimate" }, - "zip_code": { - "name": "Zip Code" - }, - "city": { - "name": "City" - }, - "annual_gross_income": { - "name": "Annual Gross Income" - }, - "annual_fees": { - "name": "Annual Fees" - }, - "annual_fee_rate": { - "name": "Annual Fee Rate" + "daily_average": { + "name": "Daily Average" } } }, + "annual_yield_sensor": { + "name": "CheckWatt Annual Net Income", + "state_attributes": {} + }, "solar_sensor": { "name": "Solar Energy" }, diff --git a/custom_components/checkwatt/translations/sv.json b/custom_components/checkwatt/translations/sv.json index cc62cb9..f35ba8a 100644 --- a/custom_components/checkwatt/translations/sv.json +++ b/custom_components/checkwatt/translations/sv.json @@ -24,9 +24,9 @@ "init": { "data": { "show_details": "Skapa energisensorer", - "show_detailed_attributes": "Visa detaljerade attribut", "push_to_cw_rank": "Skicka data till CheckWattRank", - "cm10_sensor": "Skapa CM10 sensor" + "cm10_sensor": "Skapa CM10 sensor", + "cwr_name": "Systemnamn till CheckWattRank" }, "description": "Dina val", "title": "CheckWatt" @@ -56,64 +56,32 @@ "energy_provider": { "name": "Elhandelsbolag" }, - "today_gross_income": { - "name": "Dagens Bruttoinkomst" - }, - "today_fees": { - "name": "Dagens Avgifter" - }, - "today_fees_rate": { - "name": "Dagens Avgiftssats" - }, "tomorrow_net_income": { "name": "Morgondagens Nettointäkt" }, - "tomorrow_gross_income": { - "name": "Morgondagens Bruttoinkomst" - }, - "tomorrow_fees": { - "name": "Morgondagens Avgifter" - }, - "tomorrow_fee_rate": { - "name": "Morgondagens Avgiftssats" - }, "last_update": { "name": "Senaste uppdateringen" }, "next_update": { "name": "Nästa uppdatering" - }, - "charge_peak": { - "name": "Laddningstopp" - }, - "discharge_peak": { - "name": "Urladdningstopp" } } }, - "annual_yield_sensor": { - "name": "CheckWatt Årets Intäkt", + "monthly_yield_sensor": { + "name": "CheckWatt Månadens Intäkt", "state_attributes": { - "street_address": { - "name": "Adress" + "month_estimate": { + "name": "Månadsestimat" }, - "zip_code": { - "name": "Postnummer" - }, - "city": { - "name": "Stad" - }, - "annual_gross_income": { - "name": "Årets Bruttoinkomst" - }, - "annual_fees": { - "name": "Årets Avgifter" - }, - "annual_fee_rate": { - "name": "Årets Avgiftssats" + "daily_average": { + "name": "Daglig medelintäkt" } } }, + "annual_yield_sensor": { + "name": "CheckWatt Årets Intäkt", + "state_attributes": {} + }, "solar_sensor": { "name": "Solenergi" }, @@ -172,7 +140,6 @@ "discharge_peak_dc": { "name": "Urladdningstopp DC" } - } }, "cm10_sensor": {