From 1845f4f8a93893416c5296deda3ad7c0be8cae9d Mon Sep 17 00:00:00 2001 From: Hamish Findlay Date: Tue, 28 Feb 2023 20:55:23 +0000 Subject: [PATCH] Update config flow to streamline. Add support for fixed tariffs. Add sensor to monitor battery cycles. --- custom_components/battery_sim/__init__.py | 134 +++++++++--------- custom_components/battery_sim/button.py | 2 +- custom_components/battery_sim/config_flow.py | 97 +++++-------- custom_components/battery_sim/const.py | 15 +- custom_components/battery_sim/sensor.py | 101 ++++++------- .../battery_sim/translations/en.json | 14 +- .../battery_sim/translations/nl.json | 10 +- .../battery_sim/translations/sv.json | 10 +- 8 files changed, 158 insertions(+), 225 deletions(-) diff --git a/custom_components/battery_sim/__init__.py b/custom_components/battery_sim/__init__.py index 9f63e9e..11653df 100644 --- a/custom_components/battery_sim/__init__.py +++ b/custom_components/battery_sim/__init__.py @@ -3,11 +3,11 @@ import voluptuous as vol -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import CONF_NAME from homeassistant.helpers import discovery from homeassistant.helpers.dispatcher import dispatcher_send, async_dispatcher_connect import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.start import async_at_start from homeassistant.helpers.event import async_track_state_change_event from homeassistant.core import callback @@ -16,8 +16,7 @@ UnitOfEnergy, ATTR_UNIT_OF_MEASUREMENT, STATE_UNAVAILABLE, - STATE_UNKNOWN, - EVENT_HOMEASSISTANT_START + STATE_UNKNOWN ) from .const import ( @@ -54,7 +53,12 @@ MODE_FULL, MODE_EMPTY, ATTR_MONEY_SAVED_IMPORT, - ATTR_MONEY_SAVED_EXPORT + ATTR_MONEY_SAVED_EXPORT, + TARIFF_TYPE, + NO_TARIFF_INFO, + TARIFF_SENSOR_ENTITIES, + FIXED_NUMERICAL_TARIFFS, + BATTERY_CYCLES ) _LOGGER = logging.getLogger(__name__) @@ -81,7 +85,7 @@ ) async def async_setup(hass, config): - """Set up platform from a YAML.""" + """Set up battery platforms from a YAML.""" hass.data.setdefault(DOMAIN, {}) if config.get(DOMAIN)== None: @@ -107,7 +111,7 @@ async def async_setup(hass, config): return True async def async_setup_entry(hass, entry) -> bool: - """Set up platform from a ConfigEntry.""" + """Set up battery platforms from a Config Flow Entry""" hass.data.setdefault(DOMAIN, {}) _LOGGER.debug("Setup %s.%s", DOMAIN, entry.data[CONF_NAME]) @@ -134,25 +138,22 @@ def __init__( self._hass = hass self._import_sensor_id = config[CONF_IMPORT_SENSOR] self._export_sensor_id = config[CONF_EXPORT_SENSOR] - if (CONF_SECOND_IMPORT_SENSOR not in config or - len(config[CONF_SECOND_IMPORT_SENSOR]) < 6): - self._second_import_sensor_id = None - else: + self._second_import_sensor_id = None + if (CONF_SECOND_IMPORT_SENSOR in config and + len(config[CONF_SECOND_IMPORT_SENSOR]) > 6): self._second_import_sensor_id = config[CONF_SECOND_IMPORT_SENSOR] - if (CONF_ENERGY_IMPORT_TARIFF not in config or - len(config[CONF_ENERGY_IMPORT_TARIFF]) < 6): - """For backwards compatibility""" - if (CONF_ENERGY_TARIFF not in config or - len(config[CONF_ENERGY_TARIFF]) < 6): - self._import_tariff_sensor_id = None - else: - self._import_tariff_sensor_id = config[CONF_ENERGY_TARIFF] - else: + """Defalt to sensor entites for backwards compatibility""" + self._tariff_type = TARIFF_SENSOR_ENTITIES + if TARIFF_TYPE in config: + self._tariff_type = config[TARIFF_TYPE] + self._import_tariff_sensor_id = None + if CONF_ENERGY_IMPORT_TARIFF in config: self._import_tariff_sensor_id = config[CONF_ENERGY_IMPORT_TARIFF] - if (CONF_ENERGY_EXPORT_TARIFF not in config or - len(config[CONF_ENERGY_EXPORT_TARIFF]) < 6): - self._export_tariff_sensor_id = None - else: + elif CONF_ENERGY_TARIFF in config: + """For backwards compatibility""" + self._import_tariff_sensor_id = config[CONF_ENERGY_TARIFF] + self._export_tariff_sensor_id = None + if CONF_ENERGY_EXPORT_TARIFF in config: self._export_tariff_sensor_id = config[CONF_ENERGY_EXPORT_TARIFF] self._date_recording_started = time.asctime() self._collecting1 = None @@ -188,15 +189,11 @@ def __init__( ATTR_MONEY_SAVED: 0.0, BATTERY_MODE: MODE_IDLE, ATTR_MONEY_SAVED_IMPORT: 0.0, - ATTR_MONEY_SAVED_EXPORT: 0.0 + ATTR_MONEY_SAVED_EXPORT: 0.0, + BATTERY_CYCLES: 0.0 } - self._energy_saved_today = 0.0 - self._energy_saved_week = 0.0 - self._energy_saved_month = 0.0 - self._hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, self.async_source_tracking - ) + async_at_start(self._hass, self.async_source_tracking) async_dispatcher_connect( self._hass, f"{self._name}-BatteryResetMessage", self.async_reset_battery ) @@ -208,11 +205,16 @@ def __init__( ) def async_reset_battery(self): + _LOGGER.debug("Reset battery") self.reset_import_sim_sensor() self.reset_export_sim_sensor() self._charge_state = 0.0 self._sensors[ATTR_ENERGY_SAVED] = 0.0 self._sensors[ATTR_MONEY_SAVED] = 0.0 + self._sensors[ATTR_ENERGY_BATTERY_OUT] = 0.0 + self._sensors[ATTR_ENERGY_BATTERY_IN] = 0.0 + self._sensors[ATTR_MONEY_SAVED_IMPORT] = 0.0 + self._sensors[ATTR_MONEY_SAVED_EXPORT] = 0.0 self._energy_saved_today = 0.0 self._energy_saved_week = 0.0 self._energy_saved_month = 0.0 @@ -221,6 +223,7 @@ def async_reset_battery(self): return def reset_import_sim_sensor(self): + _LOGGER.debug("Reset import sim sensor") if (self._hass.states.get(self._import_sensor_id).state is not None and self._hass.states.get(self._import_sensor_id).state not in [STATE_UNAVAILABLE, STATE_UNKNOWN]): self._sensors[GRID_IMPORT_SIM] = float(self._hass.states.get(self._import_sensor_id).state) @@ -240,7 +243,6 @@ def reset_export_sim_sensor(self): @callback def async_source_tracking(self, event): """Wait for source to be ready, then start.""" - self._collecting1 = async_track_state_change_event( self._hass, [self._import_sensor_id], self.async_import_reading ) @@ -249,7 +251,7 @@ def async_source_tracking(self, event): self._collecting3 = async_track_state_change_event( self._hass, [self._second_import_sensor_id], self.async_import_reading ) - _LOGGER.debug("<%s> monitoring %s", self._name, self._import_sensor_id) + _LOGGER.debug("<%s> monitoring %s", self._name, self._second_import_sensor_id) self._collecting2 = async_track_state_change_event( self._hass, [self._export_sensor_id], self.async_export_reading ) @@ -258,25 +260,21 @@ def async_source_tracking(self, event): @callback def async_export_reading(self, event): - """Handle the sensor state changes.""" + """Handle the source entity state changes.""" old_state = event.data.get("old_state") new_state = event.data.get("new_state") - if ( - old_state is None + if (old_state is None or new_state is None or old_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE] - or new_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE] - ): + or new_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE]): return - conversion_factor = 1.0 units = self._hass.states.get(self._export_sensor_id).attributes.get(ATTR_UNIT_OF_MEASUREMENT) + if units not in [UnitOfEnergy.KILO_WATT_HOUR, UnitOfEnergy.WATT_HOUR]: + _LOGGER.warning("Units of import sensor not recognised - may give wrong results") + conversion_factor = 1.0 if units == UnitOfEnergy.WATT_HOUR: conversion_factor = 0.001 - elif units == UnitOfEnergy.KILO_WATT_HOUR: - conversion_factor = 1.0 - else: - _LOGGER.warning("Units of import sensor not recognised - may give wrong results") export_amount = conversion_factor*(float(new_state.state) - float(old_state.state)) @@ -315,14 +313,13 @@ def async_import_reading(self, event): self._last_import_reading_time = time.time() """Check units of import sensor and calculate import amount in kWh""" - conversion_factor = 1.0 units = self._hass.states.get(self._import_sensor_id).attributes.get(ATTR_UNIT_OF_MEASUREMENT) + if units not in [UnitOfEnergy.KILO_WATT_HOUR, UnitOfEnergy.WATT_HOUR]: + _LOGGER.warning("Units of import sensor not recognised - may give wrong results") + conversion_factor = 1.0 if units == UnitOfEnergy.WATT_HOUR: conversion_factor = 0.001 - elif units == UnitOfEnergy.KILO_WATT_HOUR: - conversion_factor = 1 - else: - _LOGGER.warning("Units of import sensor not recognised - may give wrong results") + import_amount = conversion_factor*(float(new_state.state) - float(old_state.state)) self._last_import_cumulative_reading = conversion_factor*(float(new_state.state)) @@ -334,18 +331,30 @@ def async_import_reading(self, event): self.updateBattery(import_amount, self._last_export_reading) self._last_export_reading = 0.0 + def getTariffReading(self, entity_id): + if self._tariff_type == NO_TARIFF_INFO: + return None + elif self._tariff_type == FIXED_NUMERICAL_TARIFFS: + return entity_id + """Default behaviour - assume sensor entities""" + if (entity_id is None or + len(entity_id) < 6 or + self._hass.states.get(entity_id) is None or + self._hass.states.get(entity_id).state in [STATE_UNAVAILABLE, STATE_UNKNOWN]): + return None + return float(self._hass.states.get(entity_id).state) + def updateBattery(self, import_amount, export_amount): _LOGGER.debug("Battery update event (%s). Import: %s, Export: %s", self._name, round(import_amount,4), round(export_amount,4)) if self._charge_state=='unknown': self._charge_state = 0.0 - """Calculate maximum possible discharge based on battery specifications and time since last discharge""" + """Calculate maximum possible charge and discharge based on battery specifications and time since last discharge""" time_now = time.time() time_since_last_battery_update = time_now-self._last_battery_update_time max_discharge = time_since_last_battery_update*self._max_discharge_rate/3600 max_charge = time_since_last_battery_update*self._max_charge_rate/3600 available_capacity_to_charge = self._battery_size - float(self._charge_state) available_capacity_to_discharge = float(self._charge_state)*float(self._battery_efficiency) - net_money_saved = 0.0 if self._switches[PAUSE_BATTERY]: _LOGGER.debug("Battery (%s) paused.", self._name) @@ -368,7 +377,6 @@ def updateBattery(self, import_amount, export_amount): amount_to_discharge = min(max_discharge, available_capacity_to_discharge) net_export = max(amount_to_discharge - import_amount, 0) + export_amount net_import = max(import_amount - amount_to_discharge, 0) - self._charging = False self._sensors[BATTERY_MODE] = MODE_FORCE_DISCHARGING else: _LOGGER.debug("Battery (%s) normal mode.", self._name) @@ -377,21 +385,18 @@ def updateBattery(self, import_amount, export_amount): net_import = import_amount - amount_to_discharge net_export = export_amount - amount_to_charge if amount_to_charge > amount_to_discharge: - self._charging = True self._sensors[BATTERY_MODE] = MODE_CHARGING else: - self._charging = False self._sensors[BATTERY_MODE] = MODE_DISCHARGING - - if (self._import_tariff_sensor_id is not None and - self._hass.states.get(self._import_tariff_sensor_id) is not None and - self._hass.states.get(self._import_tariff_sensor_id).state not in [STATE_UNAVAILABLE, STATE_UNKNOWN]): - self._sensors[ATTR_MONEY_SAVED_IMPORT] += (import_amount - net_import)*float(self._hass.states.get(self._import_tariff_sensor_id).state) - if (self._export_tariff_sensor_id is not None and - self._hass.states.get(self._export_tariff_sensor_id) is not None and - self._hass.states.get(self._export_tariff_sensor_id).state not in [STATE_UNAVAILABLE, STATE_UNKNOWN]): - self._sensors[ATTR_MONEY_SAVED_EXPORT] += (net_export - export_amount)*float(self._hass.states.get(self._export_tariff_sensor_id).state) - if self._import_tariff_sensor_id is not None: + + current_import_tariff = self.getTariffReading(self._import_tariff_sensor_id) + current_export_tariff = self.getTariffReading(self._export_tariff_sensor_id) + + if current_import_tariff is not None: + self._sensors[ATTR_MONEY_SAVED_IMPORT] += (import_amount - net_import)*current_import_tariff + if current_export_tariff is not None: + self._sensors[ATTR_MONEY_SAVED_EXPORT] += (net_export - export_amount)*current_export_tariff + if self._tariff_type is not NO_TARIFF_INFO: self._sensors[ATTR_MONEY_SAVED] = self._sensors[ATTR_MONEY_SAVED_IMPORT] + self._sensors[ATTR_MONEY_SAVED_EXPORT] self._charge_state = float(self._charge_state) + amount_to_charge - (amount_to_discharge/float(self._battery_efficiency)) @@ -403,6 +408,7 @@ def updateBattery(self, import_amount, export_amount): self._sensors[ATTR_ENERGY_BATTERY_OUT] += amount_to_discharge self._sensors[CHARGING_RATE] = amount_to_charge/(time_since_last_battery_update/3600) self._sensors[DISCHARGING_RATE] = amount_to_discharge/(time_since_last_battery_update/3600) + self._sensors[BATTERY_CYCLES] = self._sensors[ATTR_ENERGY_BATTERY_IN] / self._battery_size self._charge_percentage = round(100*self._charge_state/self._battery_size) @@ -411,10 +417,6 @@ def updateBattery(self, import_amount, export_amount): elif self._charge_percentage >98: self._sensors[BATTERY_MODE] = MODE_FULL - self._energy_saved_today += amount_to_discharge - self._energy_saved_week += amount_to_discharge - self._energy_saved_month += amount_to_discharge - """Reset day/week/month counters""" if time.strftime("%w") != time.strftime("%w", time.gmtime(self._last_battery_update_time)): self._energy_saved_today = 0 diff --git a/custom_components/battery_sim/button.py b/custom_components/battery_sim/button.py index c9cc535..16b3cf0 100644 --- a/custom_components/battery_sim/button.py +++ b/custom_components/battery_sim/button.py @@ -92,4 +92,4 @@ def should_poll(self): return False async def async_press(self): - dispatcher_send(self.hass, f"{self._name}-BatteryResetMessage") + dispatcher_send(self.hass, f"{self._device_name}-BatteryResetMessage") diff --git a/custom_components/battery_sim/config_flow.py b/custom_components/battery_sim/config_flow.py index 694b01e..37682ad 100644 --- a/custom_components/battery_sim/config_flow.py +++ b/custom_components/battery_sim/config_flow.py @@ -2,10 +2,10 @@ import voluptuous as vol from distutils import errors from homeassistant import config_entries +from homeassistant.helpers.selector import EntitySelector, EntitySelectorConfig +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, - CONF_NAME, - UnitOfEnergy + CONF_NAME ) from .const import ( DOMAIN, @@ -85,80 +85,61 @@ async def async_step_custom(self, user_input = None): async def async_step_metertype(self, user_input = None): """Handle a flow initialized by the user.""" if user_input is not None: - if (user_input[METER_TYPE] == ONE_IMPORT_ONE_EXPORT_METER): - return await self.async_step_connectsensorsoneimport() - else: - return await self.async_step_connectsensorstwoimport() + self._data[METER_TYPE] = user_input[METER_TYPE] + self._data[TARIFF_TYPE] = user_input[TARIFF_TYPE] + return await self.async_step_connectsensors() meter_types = [ONE_IMPORT_ONE_EXPORT_METER, TWO_IMPORT_ONE_EXPORT_METER] + tariff_types = [NO_TARIFF_INFO, FIXED_NUMERICAL_TARIFFS, TARIFF_SENSOR_ENTITIES] return self.async_show_form( step_id="metertype", data_schema=vol.Schema({ vol.Required(METER_TYPE): vol.In(meter_types), - }), + vol.Required(TARIFF_TYPE): vol.In(tariff_types) + }) ) - async def async_step_connectsensorsoneimport(self, user_input = None): + async def async_step_connectsensors(self, user_input = None): if user_input is not None: self._data[CONF_IMPORT_SENSOR] = user_input[CONF_IMPORT_SENSOR] self._data[CONF_EXPORT_SENSOR] = user_input[CONF_EXPORT_SENSOR] - return await self.async_step_connecttariffsensors() + if self._data[METER_TYPE] == TWO_IMPORT_ONE_EXPORT_METER: + self._data[CONF_SECOND_IMPORT_SENSOR] = user_input[CONF_SECOND_IMPORT_SENSOR] + if self._data[TARIFF_TYPE] == NO_TARIFF_INFO: + return self.async_create_entry(title=self._data["name"], data=self._data) + else: + return await self.async_step_connecttariffsensors() - entities = self.hass.states.async_entity_ids() - energy_entities = [] - for entity_id in entities: - entity = self.hass.states.get(entity_id) - if entity.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR or entity.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.WATT_HOUR: - energy_entities.append(entity_id) - return self.async_show_form( - step_id="connectsensorsoneimport", - data_schema=vol.Schema({ - vol.Required(CONF_IMPORT_SENSOR): vol.In(energy_entities), - vol.Required(CONF_EXPORT_SENSOR): vol.In(energy_entities), - }), - ) - - async def async_step_connectsensorstwoimport(self, user_input = None): - if user_input is not None: - self._data[CONF_IMPORT_SENSOR] = user_input[CONF_IMPORT_SENSOR] - self._data[CONF_EXPORT_SENSOR] = user_input[CONF_EXPORT_SENSOR] - self._data[CONF_SECOND_IMPORT_SENSOR] = user_input[CONF_SECOND_IMPORT_SENSOR] - return await self.async_step_connecttariffsensors() + if self._data[METER_TYPE] == ONE_IMPORT_ONE_EXPORT_METER: + schema = { + vol.Required(CONF_IMPORT_SENSOR): EntitySelector(EntitySelectorConfig(device_class = SensorDeviceClass.ENERGY)), + vol.Required(CONF_EXPORT_SENSOR): EntitySelector(EntitySelectorConfig(device_class = SensorDeviceClass.ENERGY)) + } + elif self._data[METER_TYPE] == TWO_IMPORT_ONE_EXPORT_METER: + schema ={ + vol.Required(CONF_IMPORT_SENSOR): EntitySelector(EntitySelectorConfig(device_class = SensorDeviceClass.ENERGY)), + vol.Required(CONF_SECOND_IMPORT_SENSOR): EntitySelector(EntitySelectorConfig(device_class = SensorDeviceClass.ENERGY)), + vol.Required(CONF_EXPORT_SENSOR): EntitySelector(EntitySelectorConfig(device_class = SensorDeviceClass.ENERGY)) + } - entities = self.hass.states.async_entity_ids() - energy_entities = [] - for entity_id in entities: - entity = self.hass.states.get(entity_id) - if entity.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR or entity.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.WATT_HOUR: - energy_entities.append(entity_id) - return self.async_show_form( - step_id="connectsensorstwoimport", - data_schema=vol.Schema({ - vol.Required(CONF_IMPORT_SENSOR): vol.In(energy_entities), - vol.Required(CONF_SECOND_IMPORT_SENSOR): vol.In(energy_entities), - vol.Required(CONF_EXPORT_SENSOR): vol.In(energy_entities), - }), - ) + return self.async_show_form(step_id="connectsensors", data_schema=vol.Schema(schema)) async def async_step_connecttariffsensors(self, user_input = None): if user_input is not None: self._data[CONF_ENERGY_IMPORT_TARIFF] = user_input[CONF_ENERGY_IMPORT_TARIFF] - self._data[TARIFF_TYPE] = TARIFF_SENSOR_ENTITIES if CONF_ENERGY_EXPORT_TARIFF in user_input: self._data[CONF_ENERGY_EXPORT_TARIFF] = user_input[CONF_ENERGY_EXPORT_TARIFF] return self.async_create_entry(title=self._data["name"], data=self._data) + if self._data[TARIFF_TYPE] == TARIFF_SENSOR_ENTITIES: + schema = { + vol.Required(CONF_ENERGY_IMPORT_TARIFF): EntitySelector(EntitySelectorConfig()), + vol.Optional(CONF_ENERGY_EXPORT_TARIFF): EntitySelector(EntitySelectorConfig()) + } + elif self._data[TARIFF_TYPE] == FIXED_NUMERICAL_TARIFFS: + schema = { + vol.Required(CONF_ENERGY_IMPORT_TARIFF, default=0.3): vol.All(vol.Coerce(float), vol.Range(min=0, max=10)), + vol.Optional(CONF_ENERGY_EXPORT_TARIFF, default=0.3): vol.All(vol.Coerce(float), vol.Range(min=0, max=10)) + } - entities = self.hass.states.async_entity_ids() - energy_entities = [] - for entity_id in entities: - entity = self.hass.states.get(entity_id) - if entity.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR or entity.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.WATT_HOUR: - energy_entities.append(entity_id) - return self.async_show_form( - step_id="connecttariffsensors", - data_schema=vol.Schema({ - vol.Optional(CONF_ENERGY_IMPORT_TARIFF): vol.In(entities), - vol.Optional(CONF_ENERGY_EXPORT_TARIFF): vol.In(entities), - }), - ) \ No newline at end of file + return self.async_show_form(step_id="connecttariffsensors", data_schema=vol.Schema(schema)) \ No newline at end of file diff --git a/custom_components/battery_sim/const.py b/custom_components/battery_sim/const.py index ce4fbaf..f7cb434 100644 --- a/custom_components/battery_sim/const.py +++ b/custom_components/battery_sim/const.py @@ -1,5 +1,4 @@ """Constants for the battery_sim component.""" -from typing import Final DOMAIN = "battery_sim" @@ -7,15 +6,6 @@ BATTERY_PLATFORMS = ["sensor", "switch", "button"] -QUARTER_HOURLY = "quarter-hourly" -HOURLY = "hourly" -DAILY = "daily" -WEEKLY = "weekly" -MONTHLY = "monthly" -BIMONTHLY = "bimonthly" -QUARTERLY = "quarterly" -YEARLY = "yearly" - DATA_UTILITY = "battery_sim_data" SETUP_TYPE = "setup_type" @@ -55,8 +45,6 @@ ATTR_MONEY_SAVED = "total_money_saved" ATTR_MONEY_SAVED_IMPORT = "money_saved_on_imports" ATTR_MONEY_SAVED_EXPORT = "extra_money_earned_on_exports" -CHARGING = "charging" -DISCHARGING = "discharging" CHARGING_RATE = "current charging rate" DISCHARGING_RATE = "current discharging rate" ATTR_CHARGE_PERCENTAGE = "percentage" @@ -64,11 +52,14 @@ GRID_IMPORT_SIM = "simulated grid import after battery discharging" ICON_CHARGING = "mdi:battery-charging-50" ICON_DISCHARGING = "mdi:battery-50" +ICON_FULL = "mdi:battery" +ICON_EMPTY = "mdi:battery-outline" OVERIDE_CHARGING = "force_charge" FORCE_DISCHARGE = "force_discharge" PAUSE_BATTERY = "pause_battery" RESET_BATTERY = "reset_battery" PERCENTAGE_ENERGY_IMPORT_SAVED = "percentage_import_energy_saved" +BATTERY_CYCLES = "battery_cycles" BATTERY_MODE = "Battery_mode_now" MODE_IDLE = "Idle/Paused" diff --git a/custom_components/battery_sim/sensor.py b/custom_components/battery_sim/sensor.py index 0bdea8e..7422c8e 100644 --- a/custom_components/battery_sim/sensor.py +++ b/custom_components/battery_sim/sensor.py @@ -29,9 +29,6 @@ ATTR_SOURCE_ID, ATTR_STATUS, ATTR_ENERGY_SAVED, - ATTR_ENERGY_SAVED_TODAY, - ATTR_ENERGY_SAVED_WEEK, - ATTR_ENERGY_SAVED_MONTH, ATTR_DATE_RECORDING_STARTED, BATTERY_MODE, ATTR_CHARGE_PERCENTAGE, @@ -43,7 +40,14 @@ GRID_EXPORT_SIM, ICON_CHARGING, ICON_DISCHARGING, - PERCENTAGE_ENERGY_IMPORT_SAVED + ICON_FULL, + ICON_EMPTY, + PERCENTAGE_ENERGY_IMPORT_SAVED, + MODE_CHARGING, + MODE_FORCE_CHARGING, + MODE_FULL, + MODE_EMPTY, + BATTERY_CYCLES ) _LOGGER = logging.getLogger(__name__) @@ -77,6 +81,7 @@ async def define_sensors(hass, handle): sensors.append(DisplayOnlySensor(handle, DISCHARGING_RATE, SensorDeviceClass.POWER, UnitOfPower.KILO_WATT)) sensors.append(DisplayOnlySensor(handle, GRID_EXPORT_SIM, SensorDeviceClass.ENERGY, UnitOfEnergy.KILO_WATT_HOUR)) sensors.append(DisplayOnlySensor(handle, GRID_IMPORT_SIM, SensorDeviceClass.ENERGY, UnitOfEnergy.KILO_WATT_HOUR)) + sensors.append(DisplayOnlySensor(handle, BATTERY_CYCLES, None, None)) if handle._import_tariff_sensor_id != None: sensors.append(DisplayOnlySensor(handle, ATTR_MONEY_SAVED_IMPORT, SensorDeviceClass.MONETARY, hass.config.currency)) sensors.append(DisplayOnlySensor(handle, ATTR_MONEY_SAVED, SensorDeviceClass.MONETARY, hass.config.currency)) @@ -87,7 +92,10 @@ async def define_sensors(hass, handle): return sensors class DisplayOnlySensor(RestoreEntity, SensorEntity): - """Representation of a sensor which simply displays a value calculated in another sensor""" + """Representation of a sensor which simply displays a value calculated in the __init__ file""" + + _attr_should_poll = False + def __init__( self, handle, @@ -121,15 +129,15 @@ async def async_added_to_hass(self): except: _LOGGER.debug("Sensor state not restored properly.") if self._sensor_type == GRID_IMPORT_SIM: - dispatcher_send(self.hass, f"{self._name}-BatteryResetImportSim") + dispatcher_send(self.hass, f"{self._device_name}-BatteryResetImportSim") elif self._sensor_type == GRID_EXPORT_SIM: - dispatcher_send(self.hass, f"{self._name}-BatteryResetExportSim") + dispatcher_send(self.hass, f"{self._device_name}-BatteryResetExportSim") else: _LOGGER.debug("No sensor state - presume new battery.") if self._sensor_type == GRID_IMPORT_SIM: - dispatcher_send(self.hass, f"{self._name}-BatteryResetImportSim") + dispatcher_send(self.hass, f"{self._device_name}-BatteryResetImportSim") elif self._sensor_type == GRID_EXPORT_SIM: - dispatcher_send(self.hass, f"{self._name}-BatteryResetExportSim") + dispatcher_send(self.hass, f"{self._device_name}-BatteryResetExportSim") async def async_update_state(): """Update sensor state.""" @@ -137,7 +145,7 @@ async def async_update_state(): await self.async_update_ha_state(True) async_dispatcher_connect( - self.hass, f"{self._handle._name}-BatteryUpdateMessage", async_update_state + self.hass, f"{self._device_name}-BatteryUpdateMessage", async_update_state ) @property @@ -184,22 +192,11 @@ def unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._units - @property - def should_poll(self): - """No polling needed.""" - return False - @property def extra_state_attributes(self): """Return the state attributes of the sensor.""" state_attr = {} - if(self._sensor_type == ATTR_ENERGY_SAVED): - state_attr = { - ATTR_ENERGY_SAVED_TODAY: round(float(self._handle._energy_saved_today),3), - ATTR_ENERGY_SAVED_WEEK: round(float(self._handle._energy_saved_week),3), - ATTR_ENERGY_SAVED_MONTH: round(float(self._handle._energy_saved_month),3) - } - elif(self._sensor_type == GRID_IMPORT_SIM): + if(self._sensor_type == GRID_IMPORT_SIM): real_world_import = self._handle._last_import_cumulative_reading simulated_import = self._handle._sensors[GRID_IMPORT_SIM] if real_world_import==0: @@ -242,6 +239,8 @@ def available(self) -> bool: class SimulatedBattery(RestoreEntity, SensorEntity): """Representation of the battery itself""" + _attr_should_poll = False + def __init__( self, handle @@ -259,19 +258,13 @@ async def async_added_to_hass(self): self.handle._charge_state = float(state.state) if ATTR_DATE_RECORDING_STARTED in state.attributes: self.handle._date_recording_started = state.attributes[ATTR_DATE_RECORDING_STARTED] - if ATTR_ENERGY_SAVED_TODAY in state.attributes: - self._energy_saved_today = state.attributes[ATTR_ENERGY_SAVED_TODAY] - if ATTR_ENERGY_SAVED_WEEK in state.attributes: - self._energy_saved_week = state.attributes[ATTR_ENERGY_SAVED_WEEK] - if ATTR_ENERGY_SAVED_MONTH in state.attributes: - self._energy_saved_month = state.attributes[ATTR_ENERGY_SAVED_MONTH] async def async_update_state(): """Update sensor state.""" await self.async_update_ha_state(True) async_dispatcher_connect( - self.hass, f"{self.handle._name}-BatteryUpdateMessage", async_update_state + self.hass, f"{self._name}-BatteryUpdateMessage", async_update_state ) @property @@ -314,16 +307,12 @@ def state_class(self): def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return UnitOfEnergy.KILO_WATT_HOUR + @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" return UnitOfEnergy.KILO_WATT_HOUR - @property - def should_poll(self): - """No polling needed.""" - return False - @property def extra_state_attributes(self): """Return the state attributes of the sensor.""" @@ -331,9 +320,6 @@ def extra_state_attributes(self): ATTR_STATUS: self.handle._sensors[BATTERY_MODE], ATTR_CHARGE_PERCENTAGE: int(self.handle._charge_percentage), ATTR_DATE_RECORDING_STARTED: self.handle._date_recording_started, - ATTR_ENERGY_SAVED_TODAY: round(float(self.handle._energy_saved_today),3), - ATTR_ENERGY_SAVED_WEEK: round(float(self.handle._energy_saved_week),3), - ATTR_ENERGY_SAVED_MONTH: round(float(self.handle._energy_saved_month),3), CONF_BATTERY_SIZE: self.handle._battery_size, CONF_BATTERY_EFFICIENCY: float(self.handle._battery_efficiency), CONF_BATTERY_MAX_DISCHARGE_RATE: float(self.handle._max_discharge_rate), @@ -344,23 +330,25 @@ def extra_state_attributes(self): @property def icon(self): - """Return the icon to use in the frontend, if any.""" - if self.handle._charging: + """Return the icon to use in the frontend""" + if self.handle._sensors[BATTERY_MODE] in [MODE_CHARGING, MODE_FORCE_CHARGING]: return ICON_CHARGING - else: - return ICON_DISCHARGING + if self.handle._sensors[BATTERY_MODE] == MODE_FULL: + return ICON_FULL + if self.handle._sensors[BATTERY_MODE] == MODE_EMPTY: + return ICON_EMPTY + return ICON_DISCHARGING @property def state(self): """Return the state of the sensor.""" return round(float(self.handle._charge_state),3) - def update(self): - """Not used""" - class BatteryStatus(SensorEntity): """Representation of the battery itself""" + _attr_should_poll = False + def __init__( self, handle, @@ -382,7 +370,7 @@ async def async_update_state(): await self.async_update_ha_state(True) async_dispatcher_connect( - self.hass, f"{self.handle._name}-BatteryUpdateMessage", async_update_state + self.hass, f"{self._device_name}-BatteryUpdateMessage", async_update_state ) @property @@ -412,12 +400,7 @@ def native_value(self): @property def device_class(self): """Return the device class of the sensor.""" - return SensorDeviceClass.ENERGY - - @property - def should_poll(self): - """No polling needed.""" - return False + return SensorDeviceClass.ENUM @property def extra_state_attributes(self): @@ -427,16 +410,16 @@ def extra_state_attributes(self): @property def icon(self): - """Return the icon to use in the frontend, if any.""" - if self.handle._charging: + """Return the icon to use in the frontend""" + if self.handle._sensors[BATTERY_MODE] in [MODE_CHARGING, MODE_FORCE_CHARGING]: return ICON_CHARGING - else: - return ICON_DISCHARGING + if self.handle._sensors[BATTERY_MODE] == MODE_FULL: + return ICON_FULL + if self.handle._sensors[BATTERY_MODE] == MODE_EMPTY: + return ICON_EMPTY + return ICON_DISCHARGING @property def state(self): """Return the state of the sensor.""" - return self.handle._sensors[BATTERY_MODE] - - def update(self): - """Not used""" \ No newline at end of file + return self.handle._sensors[BATTERY_MODE] \ No newline at end of file diff --git a/custom_components/battery_sim/translations/en.json b/custom_components/battery_sim/translations/en.json index efb860f..0c30e42 100644 --- a/custom_components/battery_sim/translations/en.json +++ b/custom_components/battery_sim/translations/en.json @@ -33,15 +33,7 @@ }, "description": "Select the type of energy meter you have. Import meters measure energy coming into your home from the grid. You may have two of these e.g. one for day time and one for night. Export meters measure energy leaving your house to the grid e.g. excess solar generation." }, - "connectsensorsoneimport": { - "title": "Connect battery to energy meter sensors", - "data":{ - "import_sensor": "Import sensor/meter - energy coming into house", - "export_sensor" : "Export sensor/meter - energy sent to grid" - }, - "description": "When export sensor shows energy leaving the house battery will charge. When import sensor shows energy entering the house from the grid the battery will discharge." - }, - "connectsensorstwoimport": { + "connectsensors": { "title": "Connect battery to energy meter sensors", "data":{ "import_sensor": "Import sensor/meter - energy coming into house", @@ -51,12 +43,12 @@ "description": "When export sensor shows energy leaving the house battery will charge. When import sensor shows energy entering the house from the grid the battery will discharge." }, "connecttariffsensors": { - "title": "Connect sensors for energy tariffs", + "title": "Connect sensors or specify fixed value for energy tariffs", "data":{ "energy_import_tariff": "Import tariff/rate - cost of energy from the grid (£/kWh)", "energy_export_tariff": "Export tariff/rate - payment for energy exported to grid (£/kWh)" }, - "description": "For static tariffs it is best to create a helper entity with a fixed value. Currency is determined in home assistant location settings and is usually in base unit (e.g. euro) not subunit (e.g. cents)." + "description": "Currency is determined in home assistant location settings and is usually in base unit (e.g. euro) not subunit (e.g. cents) so tariff is usualy in the form 0.33 or similar." } } } diff --git a/custom_components/battery_sim/translations/nl.json b/custom_components/battery_sim/translations/nl.json index 22ac5b9..0788ce2 100644 --- a/custom_components/battery_sim/translations/nl.json +++ b/custom_components/battery_sim/translations/nl.json @@ -33,15 +33,7 @@ }, "description": "Select the type of energy meter you have. Import meters measure energy coming into your home from the grid. You may have two of these e.g. one for day time and one for night. Export meters measure energy leaving your house to the grid e.g. excess solar generation." }, - "connectsensorsoneimport": { - "title": "Energiemeter sensoren aan batterij koppelen", - "data":{ - "import_sensor": "Import sensor/meter - binnenkomende energie van het net", - "export_sensor" : "Export sensor/meter - uitgaande energie naar het net" - }, - "description": "De batterij zal opladen wanneer de export sensor aangeeft dat stroom teruggeleverd wordt aan het net. De batterij zal ontladen wanneer de import sensor aangeeft dat stroom van het net gehaald wordt." - }, - "connectsensorstwoimport": { + "connectsensors": { "title": "Connect battery to energy meter sensors", "data":{ "import_sensor": "Import sensor/meter - binnenkomende energie van het net", diff --git a/custom_components/battery_sim/translations/sv.json b/custom_components/battery_sim/translations/sv.json index 7ba2dc3..71277c6 100644 --- a/custom_components/battery_sim/translations/sv.json +++ b/custom_components/battery_sim/translations/sv.json @@ -33,15 +33,7 @@ }, "description": "Select the type of energy meter you have. Import meters measure energy coming into your home from the grid. You may have two of these e.g. one for day time and one for night. Export meters measure energy leaving your house to the grid e.g. excess solar generation." }, - "connectsensorsoneimport": { - "title": "Anslut batteri eller energi mätarare", - "data":{ - "import_sensor": "Importera senor/mätare - inkommande energi", - "export_sensor" : "Exportera sensor/mätare - producering till elnätet" - }, - "description": "När exportsensorn visar energi som lämnar huset laddas batteriet. När importsensorn visar energi som kommer in i huset från nätet laddas batteriet ur." - }, - "connectsensorstwoimport": { + "connectsensors": { "title": "Anslut batteri eller energi mätarare", "data":{ "import_sensor": "Importera senor/mätare - inkommande energi",