From c13c2af75a25470d00c5addcfe3df44801906156 Mon Sep 17 00:00:00 2001 From: hif2k1 Date: Mon, 4 Dec 2023 21:01:25 +0000 Subject: [PATCH 1/3] Permit monitoring of unlimited meters (#104) * Standardise input sensor/meter handelling to permitt unlimited meters * Bug fixes * Add discharge only option * Add battery configuration options * Update the confid directly instead of using options * Format with black * Adjust wording of config_flow --------- Co-authored-by: Hamish Findlay --- custom_components/battery_sim/__init__.py | 455 +++++++----------- custom_components/battery_sim/button.py | 5 +- custom_components/battery_sim/config_flow.py | 435 ++++++++++++----- custom_components/battery_sim/const.py | 29 +- custom_components/battery_sim/helpers.py | 84 ++++ custom_components/battery_sim/select.py | 101 ++++ custom_components/battery_sim/sensor.py | 206 ++++---- custom_components/battery_sim/switch.py | 12 +- .../battery_sim/translations/en.json | 139 +++++- .../battery_sim/translations/nl.json | 101 ++-- .../battery_sim/translations/sv.json | 101 ++-- 11 files changed, 1025 insertions(+), 643 deletions(-) create mode 100644 custom_components/battery_sim/helpers.py create mode 100644 custom_components/battery_sim/select.py diff --git a/custom_components/battery_sim/__init__.py b/custom_components/battery_sim/__init__.py index df7def6..6f1dfa6 100644 --- a/custom_components/battery_sim/__init__.py +++ b/custom_components/battery_sim/__init__.py @@ -1,6 +1,7 @@ """Simulates a battery to evaluate how much energy it could save.""" import logging import time +import asyncio import voluptuous as vol @@ -9,10 +10,7 @@ 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.helpers.dispatcher import ( - dispatcher_send, - async_dispatcher_connect -) +from homeassistant.helpers.dispatcher import dispatcher_send, async_dispatcher_connect from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, @@ -44,16 +42,12 @@ CONF_ENERGY_TARIFF, CONF_EXPORT_SENSOR, CONF_IMPORT_SENSOR, - CONF_SECOND_EXPORT_SENSOR, - CONF_SECOND_IMPORT_SENSOR, + CONF_UPDATE_FREQUENCY, + CONF_INPUT_LIST, + DISCHARGE_ONLY, DISCHARGING_RATE, DOMAIN, - FIXED_NUMERICAL_TARIFFS, FORCE_DISCHARGE, - GRID_EXPORT_SIM, - GRID_IMPORT_SIM, - MESSAGE_TYPE_BATTERY_RESET_IMP, - MESSAGE_TYPE_BATTERY_RESET_EXP, MESSAGE_TYPE_BATTERY_UPDATE, MESSAGE_TYPE_GENERAL, MODE_CHARGING, @@ -66,9 +60,16 @@ NO_TARIFF_INFO, OVERIDE_CHARGING, PAUSE_BATTERY, - TARIFF_SENSOR_ENTITIES, + FIXED_TARIFF, TARIFF_TYPE, + SENSOR_ID, + SENSOR_TYPE, + TARIFF_SENSOR, + IMPORT, + EXPORT, + SIMULATED_SENSOR, ) +from .helpers import generate_input_list BATTERY_CONFIG_SCHEMA = vol.Schema( vol.All( @@ -88,10 +89,7 @@ ) CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema({cv.slug: BATTERY_CONFIG_SCHEMA}) - }, - extra=vol.ALLOW_EXTRA + {DOMAIN: vol.Schema({cv.slug: BATTERY_CONFIG_SCHEMA})}, extra=vol.ALLOW_EXTRA ) _LOGGER = logging.getLogger(__name__) @@ -110,7 +108,6 @@ async def async_setup(hass, config): if battery in hass.data[DOMAIN]: _LOGGER.warning("Battery name not unique - not able to create.") continue - hass.data[DOMAIN][battery] = handle for platform in BATTERY_PLATFORMS: @@ -119,12 +116,7 @@ async def async_setup(hass, config): hass, platform, DOMAIN, - [ - { - CONF_BATTERY: battery, - CONF_NAME: conf.get(CONF_NAME, battery) - } - ], + [{CONF_BATTERY: battery, CONF_NAME: conf.get(CONF_NAME, battery)}], config, ) ) @@ -139,8 +131,8 @@ async def async_setup_entry(hass, entry) -> bool: handle = SimulatedBatteryHandle(entry.data, hass) hass.data[DOMAIN][entry.entry_id] = handle + handle._listeners.append(entry.add_update_listener(async_update_settings)) - # Forward the setup to the sensor platform. for platform in BATTERY_PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, platform) @@ -148,6 +140,38 @@ async def async_setup_entry(hass, entry) -> bool: return True +async def async_update_settings(hass, entry): + _LOGGER.warning(f"Config change detected {entry.data[CONF_NAME]}") + await hass.config_entries.async_reload(entry.entry_id) + return + + +async def async_unload_entry(hass, config_entry): + """Unload a config entry""" + # Unload a config entry + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(config_entry, platform) + for platform in BATTERY_PLATFORMS + ] + ) + ) + + """Remove listeners""" + handle = hass.data[DOMAIN][config_entry.entry_id] + for listener in handle._listeners: + if listener is not None: + outcome = listener() + _LOGGER.warning(f"unloading listener: {outcome}") + + _LOGGER.debug("Unload integration") + if unload_ok: + hass.data[DOMAIN].pop(config_entry.entry_id) + + return unload_ok + + class SimulatedBatteryHandle: """Representation of the battery itself.""" @@ -155,65 +179,37 @@ def __init__(self, config, hass): """Initialize the Battery.""" self._hass = hass self._date_recording_started = time.asctime() - self._name = config[CONF_NAME] - self._battery_size = config[CONF_BATTERY_SIZE] - self._max_discharge_rate = config[CONF_BATTERY_MAX_DISCHARGE_RATE] - self._max_charge_rate = config[CONF_BATTERY_MAX_CHARGE_RATE] - self._battery_efficiency = config[CONF_BATTERY_EFFICIENCY] - self._sensor_collection: list = [] - - self._export_sensor_id: str = config[CONF_EXPORT_SENSOR] - self._export_tariff_sensor_id: str = None - - self._import_sensor_id: str = config[CONF_IMPORT_SENSOR] - self._import_tariff_sensor_id: str = None - self._charging: bool = False - - self._last_import_reading_time = time.time() - self._last_export_reading_time = time.time() + self._accumulated_import_reading: float = 0.0 self._last_battery_update_time = time.time() - self._max_discharge: float = 0.0 self._charge_percentage: float = 0.0 self._charge_state: float = 0.0 - self._last_export_reading: float = 0.0 - self._last_import_cumulative_reading: float = 1.0 - - self._second_import_sensor_id: str = ( - config.get(CONF_SECOND_IMPORT_SENSOR) - if len(config.get(CONF_SECOND_IMPORT_SENSOR, "")) > 6 - else None - ) - - self._second_export_sensor_id: str = ( - config.get(CONF_SECOND_EXPORT_SENSOR) - if len(config.get(CONF_SECOND_EXPORT_SENSOR, "")) > 6 - else None - ) - - """Default sensor entities for backwards compatibility""" - self._tariff_type: str = TARIFF_SENSOR_ENTITIES + self._accumulated_export_reading: float = 0.0 + self._last_import_reading_sensor_data: str = None + self._last_export_reading_sensor_data: str = None + self._listeners = [] - if TARIFF_TYPE in config: - self._tariff_type = config[TARIFF_TYPE] - - if CONF_ENERGY_IMPORT_TARIFF in config: - self._import_tariff_sensor_id = config[CONF_ENERGY_IMPORT_TARIFF] - elif CONF_ENERGY_TARIFF in config: - """For backwards compatibility""" - self._import_tariff_sensor_id = config[CONF_ENERGY_TARIFF] - - if CONF_ENERGY_EXPORT_TARIFF in config: - self._export_tariff_sensor_id = config[CONF_ENERGY_EXPORT_TARIFF] + self._battery_size = config[CONF_BATTERY_SIZE] + if self._charge_state > self._battery_size: + self._charge_state = self._battery_size + self._max_discharge_rate = config[CONF_BATTERY_MAX_DISCHARGE_RATE] + self._max_charge_rate = config[CONF_BATTERY_MAX_CHARGE_RATE] + self._battery_efficiency = config[CONF_BATTERY_EFFICIENCY] + if CONF_INPUT_LIST in config: + self._inputs = config[CONF_INPUT_LIST] + else: + """Needed for backwards compatability""" + self._inputs = generate_input_list(config=config) self._switches: dict = { OVERIDE_CHARGING: False, PAUSE_BATTERY: False, FORCE_DISCHARGE: False, CHARGE_ONLY: False, + DISCHARGE_ONLY: False, } self._sensors: dict = { @@ -222,48 +218,30 @@ def __init__(self, config, hass): ATTR_ENERGY_BATTERY_IN: 0.0, CHARGING_RATE: 0.0, DISCHARGING_RATE: 0.0, - GRID_EXPORT_SIM: 0.0, - GRID_IMPORT_SIM: 0.0, ATTR_MONEY_SAVED: 0.0, BATTERY_MODE: MODE_IDLE, ATTR_MONEY_SAVED_IMPORT: 0.0, ATTR_MONEY_SAVED_EXPORT: 0.0, BATTERY_CYCLES: 0.0, } + for input_details in self._inputs: + self._sensors[input_details[SIMULATED_SENSOR]] = None async_at_start(self._hass, self.async_source_tracking) - async_dispatcher_connect( - self._hass, - f"{self._name}-{MESSAGE_TYPE_GENERAL}", - self.async_reset_battery - ) - - async_dispatcher_connect( - self._hass, - f"{self._name}-{MESSAGE_TYPE_BATTERY_RESET_IMP}", - self.reset_sim_sensor, - ) - - async_dispatcher_connect( - self._hass, - f"{self._name}-{MESSAGE_TYPE_BATTERY_RESET_EXP}", - self.reset_sim_sensor, + self._listeners.append( + async_dispatcher_connect( + self._hass, + f"{self._name}-{MESSAGE_TYPE_GENERAL}", + self.async_reset_battery, + ) ) def async_reset_battery(self): """Reset the battery to start over.""" _LOGGER.debug("Reset battery") - self.reset_sim_sensor( - GRID_IMPORT_SIM, - self._import_sensor_id, - self._second_import_sensor_id - ) - self.reset_sim_sensor( - GRID_EXPORT_SIM, - self._export_sensor_id, - self._second_export_sensor_id - ) + for input in self._inputs: + self.reset_sim_sensor(input[SIMULATED_SENSOR]) self._charge_state = 0.0 @@ -279,115 +257,64 @@ def async_reset_battery(self): self._energy_saved_month = 0.0 self._date_recording_started = time.asctime() - dispatcher_send( - self._hass, - f"{self._name}-{MESSAGE_TYPE_BATTERY_UPDATE}" - ) + dispatcher_send(self._hass, f"{self._name}-{MESSAGE_TYPE_BATTERY_UPDATE}") return - def reset_sim_sensor( - self, - target_sensor_key, - primary_sensor_id, - secondary_sensor_id - ): - """Reset the Primary and Secondary Sensor.""" + def reset_sim_sensor(self, target_sensor_key): + """Reset the Simulated Sensor.""" _LOGGER.debug(f"Reset {target_sensor_key} sim sensor") - sensor_ids = [primary_sensor_id] - - if secondary_sensor_id is not None: - sensor_ids.append(secondary_sensor_id) + self._sensors[target_sensor_key] = 0.0 - total_sim = 0.0 + for input_details in self._inputs: + if input_details[SIMULATED_SENSOR] == target_sensor_key: + _LOGGER.warning(input_details[SENSOR_ID]) + if self._hass.states.get(input_details[SENSOR_ID]).state not in [ + STATE_UNAVAILABLE, + STATE_UNKNOWN, + ]: + self._sensors[target_sensor_key] = float( + self._hass.states.get(input_details[SENSOR_ID]).state + ) - for sid in sensor_ids: - if self._hass.states.get(sid).state not in [ - STATE_UNAVAILABLE, - STATE_UNKNOWN, - ]: - total_sim += float(self._hass.states.get(sid).state) - - self._sensors[target_sensor_key] = total_sim - dispatcher_send( - self._hass, - f"{self._name}-{MESSAGE_TYPE_BATTERY_UPDATE}" - ) + dispatcher_send(self._hass, f"{self._name}-{MESSAGE_TYPE_BATTERY_UPDATE}") @callback def async_source_tracking(self, event): """Wait for source to be ready, then start.""" - def start_sensor_tracking( - sensor_id: str, - collection: str, - reading_function, - is_import: bool - ): + for input_details in self._inputs: """Start tracking state changes for a sensor.""" - getattr(self, collection).append( + self._listeners.append( async_track_state_change_event( - self._hass, - [sensor_id], - lambda event: reading_function(event, is_import), + self._hass, [input_details[SENSOR_ID]], self.async_reading_handler ) ) - - _LOGGER.debug("(%s) monitoring %s", self._name, sensor_id) - - start_sensor_tracking( - sensor_id=self._import_sensor_id, - collection="_sensor_collection", - reading_function=self.async_reading_handler, - is_import=True, - ) - - start_sensor_tracking( - sensor_id=self._export_sensor_id, - collection="_sensor_collection", - reading_function=self.async_reading_handler, - is_import=False, - ) - - if self._second_import_sensor_id is not None: - start_sensor_tracking( - sensor_id=self._second_import_sensor_id, - collection="_sensor_collection", - reading_function=self.async_reading_handler, - is_import=True, - ) - - if self._second_import_sensor_id is not None: - start_sensor_tracking( - sensor_id=self._second_export_sensor_id, - collection="_sensor_collection", - reading_function=self.async_reading_handler, - is_import=False, - ) - - _LOGGER.debug( - "(%s) monitoring %s", self._name, "Done adding the Sensor tracking" - ) + _LOGGER.debug(f"{self._name} monitoring {input_details[SENSOR_ID]}") return @callback def async_reading_handler( self, event, - is_import ): - """Handle the sensor state changes for import or export.""" - sensor_charge_rate = DISCHARGING_RATE if is_import else CHARGING_RATE - sensor_type = "import" if is_import else "export" - - cumulative_reading_attr = f"_last_{sensor_type}_cumulative_reading" - last_reading_time_attr = f"_last_{sensor_type}_reading_time" + sensor_id = event.data.get("entity_id") + for input_details in self._inputs: + if sensor_id == input_details[SENSOR_ID]: + break + else: + _LOGGER.warning( + f"Error reading input sensor {sensor_id} not found in input sensors" + ) + return - last_reading_time = time.time() + """Handle the sensor state changes for import or export.""" + sensor_charge_rate = ( + DISCHARGING_RATE if input_details[SENSOR_TYPE] == IMPORT else CHARGING_RATE + ) old_state = event.data.get("old_state") new_state = event.data.get("new_state") - sensor_id = event.data.get("entity_id") if ( old_state is None @@ -404,16 +331,15 @@ def async_reading_handler( ) if units in [UnitOfEnergy.KILO_WATT_HOUR, UnitOfEnergy.WATT_HOUR]: - conversion_factor = ( - 1.0 if units == UnitOfEnergy.KILO_WATT_HOUR else 0.001 - ) - unit_of_energy = ( - "kWh" if units == UnitOfEnergy.KILO_WATT_HOUR else "Wh" - ) + conversion_factor = 1.0 if units == UnitOfEnergy.KILO_WATT_HOUR else 0.001 + unit_of_energy = "kWh" if units == UnitOfEnergy.KILO_WATT_HOUR else "Wh" new_state_value = float(new_state.state) * conversion_factor old_state_value = float(old_state.state) * conversion_factor + if self._sensors[input_details[SIMULATED_SENSOR]] is None: + self._sensors[input_details[SIMULATED_SENSOR]] = old_state_value + if new_state_value == old_state_value: # _LOGGER.debug("(%s) No change in readings .. ", self._name) return @@ -421,64 +347,57 @@ def async_reading_handler( reading_variance = new_state_value - old_state_value _LOGGER.debug( - f"({self._name}) {sensor_id} {is_import}: {old_state_value} {unit_of_energy} => {new_state_value} {unit_of_energy} = Δ {reading_variance} {unit_of_energy}" + f"({self._name}) {sensor_id} {input_details[SENSOR_TYPE]}: {old_state_value} {unit_of_energy} => {new_state_value} {unit_of_energy} = Δ {reading_variance} {unit_of_energy}" ) if reading_variance < 0: _LOGGER.warning( "(%s) %s sensor value decreased - meter may have been reset", self._name, - sensor_type, + input_details[SENSOR_TYPE], ) self._sensors[sensor_charge_rate] = 0 + return - if reading_variance >= 0: - setattr(self, cumulative_reading_attr, new_state_value) - - if is_import: - self.update_battery( - reading_variance, - self._last_export_reading - ) - self._last_export_reading = 0.0 - - if not is_import: - if self._last_import_reading_time > self._last_export_reading_time: - if self._last_export_reading > 0: - _LOGGER.warning( - "(%s) Accumulated export reading not cleared error", - self._last_export_reading, - ) + if input_details[SENSOR_TYPE] == IMPORT: + self._last_import_reading_sensor_data = input_details + self._accumulated_import_reading += reading_variance - self._last_export_reading = reading_variance + if input_details[SENSOR_TYPE] == EXPORT: + self._last_export_reading_sensor_data = input_details + self._accumulated_export_reading += reading_variance - else: - reading_variance += self._last_export_reading - self._last_export_reading = 0.0 - self.update_battery(0.0, reading_variance) + time_since_battery_update = time.time() - self._last_battery_update_time + if time_since_battery_update > 60: + self.update_battery( + self._accumulated_import_reading, self._accumulated_export_reading + ) + self._accumulated_export_reading = 0.0 + self._accumulated_import_reading = 0.0 - # Finish the Sensor Reading - setattr(self, last_reading_time_attr, last_reading_time) return - def get_tariff_information(self, entity_id): + def get_tariff_information(self, input_details): + if input_details is None: + return None """Get Tarrif information to be used for calculating.""" - if self._tariff_type == NO_TARIFF_INFO: + if input_details[TARIFF_TYPE] == NO_TARIFF_INFO: return None - elif self._tariff_type == FIXED_NUMERICAL_TARIFFS: - return entity_id + elif input_details[TARIFF_TYPE] == FIXED_TARIFF: + return input_details[FIXED_TARIFF] # Default behavior - 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 + TARIFF_SENSOR not in input_details + or input_details[TARIFF_SENSOR] is None + or len(input_details[TARIFF_SENSOR]) < 6 + or self._hass.states.get(input_details[TARIFF_SENSOR]) is None + or self._hass.states.get(input_details[TARIFF_SENSOR]).state in [STATE_UNAVAILABLE, STATE_UNKNOWN] ): return None - return float(self._hass.states.get(entity_id).state) + return float(self._hass.states.get(input_details[TARIFF_SENSOR]).state) def update_battery(self, import_amount, export_amount): """Update battery statistics based on the reading for Im- or Export.""" @@ -499,8 +418,9 @@ def update_battery(self, import_amount, export_amount): time_since_last_battery_update = time_now - time_last_update _LOGGER.debug( - "(%s) Import: (%s) Export: (%s) .... Timing: %s = Now / %s = Last Update / %s Time (sec).", + "(%s), Size: (%s)kWh, Import: (%s), Export: (%s), .... Timing: %s = Now / %s = Last Update / %s Time (sec).", self._name, + self._battery_size, import_amount, export_amount, time_now, @@ -511,13 +431,9 @@ def update_battery(self, import_amount, export_amount): max_discharge = time_since_last_battery_update * ( self._max_discharge_rate / 3600 ) - max_charge = time_since_last_battery_update * ( - self._max_charge_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_charge = self._battery_size - float(self._charge_state) available_capacity_to_discharge = float(self._charge_state) * float( self._battery_efficiency @@ -551,25 +467,17 @@ def update_battery(self, import_amount, export_amount): _LOGGER.debug("(%s) Battery overide charging.", self._name) amount_to_charge = min(max_charge, available_capacity_to_charge) amount_to_discharge = 0.0 - net_export = ( - max(export_amount - amount_to_charge, 0) - ) + net_export = max(export_amount - amount_to_charge, 0) - net_import = ( - max(amount_to_charge - export_amount, 0) + import_amount - ) + net_import = max(amount_to_charge - export_amount, 0) + import_amount self._charging = True self._sensors[BATTERY_MODE] = MODE_FORCE_CHARGING elif self._switches[FORCE_DISCHARGE]: _LOGGER.debug("(%s) Battery forced discharging.", self._name) amount_to_charge = 0.0 - amount_to_discharge = ( - min(max_discharge, available_capacity_to_discharge) - ) - net_export = ( - max(amount_to_discharge - import_amount, 0) + 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._sensors[BATTERY_MODE] = MODE_FORCE_DISCHARGING @@ -578,20 +486,32 @@ def update_battery(self, import_amount, export_amount): amount_to_charge: float = min( export_amount, max_charge, available_capacity_to_charge ) - amount_to_discharge: float = 0.0 net_import = import_amount net_export = export_amount - amount_to_charge - if amount_to_charge > amount_to_discharge: + if amount_to_charge > 0.0: self._sensors[BATTERY_MODE] = MODE_CHARGING else: self._sensors[BATTERY_MODE] = MODE_IDLE + elif self._switches[DISCHARGE_ONLY]: + _LOGGER.debug("(%s) Battery discharge only mode.", self._name) + amount_to_charge: float = 0.0 + amount_to_discharge = min( + import_amount, max_discharge, available_capacity_to_discharge + ) + net_import = import_amount - amount_to_discharge + net_export = export_amount + if amount_to_discharge > 0.0: + self._sensors[BATTERY_MODE] = MODE_DISCHARGING + else: + self._sensors[BATTERY_MODE] = MODE_IDLE + current_import_tariff = self.get_tariff_information( - self._import_tariff_sensor_id + self._last_import_reading_sensor_data ) current_export_tariff = self.get_tariff_information( - self._export_tariff_sensor_id + self._last_export_reading_sensor_data ) if current_import_tariff is not None: @@ -602,11 +522,10 @@ def update_battery(self, import_amount, export_amount): 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._sensors[ATTR_MONEY_SAVED] = ( + self._sensors[ATTR_MONEY_SAVED_IMPORT] + + self._sensors[ATTR_MONEY_SAVED_EXPORT] + ) self._charge_state = ( float(self._charge_state) @@ -615,8 +534,18 @@ def update_battery(self, import_amount, export_amount): ) self._sensors[ATTR_ENERGY_SAVED] += import_amount - net_import - self._sensors[GRID_IMPORT_SIM] += net_import - self._sensors[GRID_EXPORT_SIM] += net_export + + if self._last_import_reading_sensor_data is not None: + self._sensors[ + self._last_import_reading_sensor_data[SIMULATED_SENSOR] + ] += net_import + _LOGGER.warning(f"Updating simulated export sensor: baterry: {self._name}, last_export_sensor:{self._last_export_reading_sensor_data}") + if self._last_export_reading_sensor_data is not None: + _LOGGER.warning(f"Updating simulated export sensor: baterry: {self._name}, last_export_sensor:{self._last_export_reading_sensor_data}, last_export_sensor[simulated_sensor]:{self._last_export_reading_sensor_data[SIMULATED_SENSOR]}") + self._sensors[ + self._last_export_reading_sensor_data[SIMULATED_SENSOR] + ] += net_export + self._sensors[ATTR_ENERGY_BATTERY_IN] += amount_to_charge self._sensors[ATTR_ENERGY_BATTERY_OUT] += amount_to_discharge @@ -630,9 +559,7 @@ def update_battery(self, import_amount, export_amount): self._sensors[ATTR_ENERGY_BATTERY_IN] / self._battery_size ) - self._charge_percentage = round( - 100 * self._charge_state / self._battery_size - ) + self._charge_percentage = round(100 * self._charge_state / self._battery_size) if self._charge_percentage < 2: self._sensors[BATTERY_MODE] = MODE_EMPTY @@ -640,27 +567,15 @@ def update_battery(self, import_amount, export_amount): self._sensors[BATTERY_MODE] = MODE_FULL """Reset day/week/month counters""" - if ( - time.strftime("%w") - != time.strftime("%w", time.gmtime(time_last_update)) - ): + if time.strftime("%w") != time.strftime("%w", time.gmtime(time_last_update)): self._energy_saved_today = 0 - if ( - time.strftime("%U") - != time.strftime("%U", time.gmtime(time_last_update)) - ): + if time.strftime("%U") != time.strftime("%U", time.gmtime(time_last_update)): self._energy_saved_week = 0 - if ( - time.strftime("%m") - != time.strftime("%m", time.gmtime(time_last_update)) - ): + if time.strftime("%m") != time.strftime("%m", time.gmtime(time_last_update)): self._energy_saved_month = 0 self._last_battery_update_time = time_now - dispatcher_send( - self._hass, - f"{self._name}-{MESSAGE_TYPE_BATTERY_UPDATE}" - ) + dispatcher_send(self._hass, f"{self._name}-{MESSAGE_TYPE_BATTERY_UPDATE}") _LOGGER.debug("(%s) Battery update complete.", self._name) diff --git a/custom_components/battery_sim/button.py b/custom_components/battery_sim/button.py index 489a9cf..3d06029 100644 --- a/custom_components/battery_sim/button.py +++ b/custom_components/battery_sim/button.py @@ -89,7 +89,4 @@ def should_poll(self): return False async def async_press(self): - dispatcher_send( - self.hass, - f"{self._device_name}-{MESSAGE_TYPE_GENERAL}" - ) + dispatcher_send(self.hass, f"{self._device_name}-{MESSAGE_TYPE_GENERAL}") diff --git a/custom_components/battery_sim/config_flow.py b/custom_components/battery_sim/config_flow.py index ac71808..2680d5d 100644 --- a/custom_components/battery_sim/config_flow.py +++ b/custom_components/battery_sim/config_flow.py @@ -1,11 +1,13 @@ """Configuration flow for the Battery.""" import logging import voluptuous as vol +import time from homeassistant import config_entries from homeassistant.helpers.selector import EntitySelector, EntitySelectorConfig from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import CONF_NAME +from homeassistant.core import callback from .const import ( DOMAIN, BATTERY_OPTIONS, @@ -14,34 +16,37 @@ CONF_BATTERY_MAX_DISCHARGE_RATE, CONF_BATTERY_MAX_CHARGE_RATE, CONF_BATTERY_EFFICIENCY, + CONF_INPUT_LIST, CONF_UNIQUE_NAME, - CONF_IMPORT_SENSOR, - CONF_SECOND_IMPORT_SENSOR, - CONF_EXPORT_SENSOR, - CONF_SECOND_EXPORT_SENSOR, - CONF_ENERGY_IMPORT_TARIFF, - CONF_ENERGY_EXPORT_TARIFF, SETUP_TYPE, CONFIG_FLOW, - METER_TYPE, - ONE_IMPORT_ONE_EXPORT_METER, - TWO_IMPORT_ONE_EXPORT_METER, - TWO_IMPORT_TWO_EXPORT_METER, TARIFF_TYPE, NO_TARIFF_INFO, - TARIFF_SENSOR_ENTITIES, - FIXED_NUMERICAL_TARIFFS, + IMPORT, + EXPORT, + SENSOR_ID, + SENSOR_TYPE, + TARIFF_SENSOR, + FIXED_TARIFF, + SIMULATED_SENSOR, ) +from .helpers import generate_input_list _LOGGER = logging.getLogger(__name__) @config_entries.HANDLERS.register(DOMAIN) -class ExampleConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """Example config flow.""" +class BatterySetupConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Config flow.""" VERSION = 1 + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Return flow options.""" + return BatteryOptionsFlowHandler(config_entry) + async def async_step_user(self, user_input): """Handle a flow initialized by the user.""" if user_input is not None: @@ -53,7 +58,8 @@ async def async_step_user(self, user_input): self._data[CONF_NAME] = f"{DOMAIN}: { user_input[BATTERY_TYPE]}" await self.async_set_unique_id(self._data[CONF_NAME]) self._abort_if_unique_id_configured() - return await self.async_step_metertype() + self._data[CONF_INPUT_LIST] = [] + return await self.async_meter_menu() battery_options_names = list(BATTERY_OPTIONS) return self.async_show_form( @@ -72,7 +78,7 @@ async def async_step_custom(self, user_input=None): self._data[CONF_NAME] = f"{DOMAIN}: {self._data[CONF_UNIQUE_NAME]}" await self.async_set_unique_id(self._data[CONF_NAME]) self._abort_if_unique_id_configured() - return await self.async_step_metertype() + return await self.async_meter_menu() return self.async_show_form( step_id="custom", @@ -93,129 +99,320 @@ 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.""" + async def async_meter_menu(self, user_input=None): + menu_options = ["add_import_meter", "add_export_meter"] + import_meter: bool = False + export_meter: bool = False + for input in self._data[CONF_INPUT_LIST]: + if input[SENSOR_TYPE] == IMPORT: + import_meter = True + if input[SENSOR_TYPE] == EXPORT: + export_meter = True + if import_meter and export_meter: + menu_options.append("all_done") + return self.async_show_menu(step_id="meterMenu", menu_options=menu_options) + + async def async_step_add_import_meter(self, user_input=None): if user_input is not None: - 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, - TWO_IMPORT_TWO_EXPORT_METER, - ] - tariff_types = [ - NO_TARIFF_INFO, - FIXED_NUMERICAL_TARIFFS, - TARIFF_SENSOR_ENTITIES - ] + self.current_input_entry: dict = { + SENSOR_ID: user_input[SENSOR_ID], + SENSOR_TYPE: IMPORT, + SIMULATED_SENSOR: f"simulated_{user_input[SENSOR_ID]}", + } + return await self.async_tariff_menu() return self.async_show_form( - step_id="metertype", + step_id="add_import_meter", data_schema=vol.Schema( { - vol.Required(METER_TYPE): vol.In(meter_types), - vol.Required(TARIFF_TYPE): vol.In(tariff_types), + vol.Required(SENSOR_ID): EntitySelector( + EntitySelectorConfig(device_class=SensorDeviceClass.ENERGY) + ), } ), ) - async def async_step_connectsensors(self, user_input=None): + async def async_step_add_export_meter(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] - 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[METER_TYPE] == TWO_IMPORT_TWO_EXPORT_METER: - self._data[CONF_SECOND_IMPORT_SENSOR] = user_input[ - CONF_SECOND_IMPORT_SENSOR - ] - self._data[CONF_SECOND_EXPORT_SENSOR] = user_input[ - CONF_SECOND_EXPORT_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() - - 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) - ), - } - elif self._data[METER_TYPE] == TWO_IMPORT_TWO_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) - ), - vol.Required(CONF_SECOND_EXPORT_SENSOR): EntitySelector( - EntitySelectorConfig(device_class=SensorDeviceClass.ENERGY) - ), + self.current_input_entry: dict = { + SENSOR_ID: user_input[SENSOR_ID], + SENSOR_TYPE: EXPORT, + SIMULATED_SENSOR: f"simulated_{user_input[SENSOR_ID]}", } + return await self.async_tariff_menu() + + return self.async_show_form( + step_id="add_export_meter", + data_schema=vol.Schema( + { + vol.Required(SENSOR_ID): EntitySelector( + EntitySelectorConfig(device_class=SensorDeviceClass.ENERGY) + ), + } + ), + ) + + async def async_tariff_menu(self, user_input=None): + return self.async_show_menu( + step_id="tariffMenu", + menu_options=["no_tariff_info", "fixed_tariff", "tariff_sensor"], + ) + + async def async_step_no_tariff_info(self, user_input=None): + self.current_input_entry[TARIFF_TYPE] = NO_TARIFF_INFO + self._data[CONF_INPUT_LIST].append(self.current_input_entry) + return await self.async_meter_menu() + + async def async_step_fixed_tariff(self, user_input=None): + if user_input is not None: + self.current_input_entry[TARIFF_TYPE] = FIXED_TARIFF + self.current_input_entry[FIXED_TARIFF] = user_input[FIXED_TARIFF] + self._data[CONF_INPUT_LIST].append(self.current_input_entry) + return await self.async_meter_menu() + + return self.async_show_form( + step_id="fixed_tariff", + data_schema=vol.Schema( + { + vol.Optional(FIXED_TARIFF): vol.All( + vol.Coerce(float), vol.Range(min=0, max=10) + ) + } + ), + ) + + async def async_step_tariff_sensor(self, user_input=None): + if user_input is not None: + self.current_input_entry[TARIFF_TYPE] = TARIFF_SENSOR + self.current_input_entry[TARIFF_SENSOR] = user_input[TARIFF_SENSOR] + self._data[CONF_INPUT_LIST].append(self.current_input_entry) + return await self.async_meter_menu() return self.async_show_form( - step_id="connectsensors", data_schema=vol.Schema(schema) + step_id="tariff_sensor", + data_schema=vol.Schema( + {vol.Required(TARIFF_SENSOR): EntitySelector(EntitySelectorConfig())} + ), + ) + + async def async_step_all_done(self, user_input=None): + return self.async_create_entry(title=self._data[CONF_NAME], data=self._data) + + +class BatteryOptionsFlowHandler(config_entries.OptionsFlow): + """Handle a option flow for battery.""" + + def __init__(self, config_entry: config_entries.ConfigEntry): + """Initialize options flow.""" + self.config_entry = config_entry + self.updated_entry = config_entry.data.copy() + if CONF_INPUT_LIST not in self.updated_entry: + self.updated_entry[CONF_INPUT_LIST] = generate_input_list( + config=self.updated_entry + ) + + async def async_step_init(self, user_input=None): + """Handle options flow.""" + return self.async_show_menu( + step_id="init", menu_options=["main_params", "input_sensors", "all_done"] ) - async def async_step_connecttariffsensors(self, user_input=None): + async def async_step_main_params(self, user_input=None): if user_input is not None: - self._data[CONF_ENERGY_IMPORT_TARIFF] = user_input[ - CONF_ENERGY_IMPORT_TARIFF + self.updated_entry[CONF_BATTERY_SIZE] = user_input[CONF_BATTERY_SIZE] + self.updated_entry[CONF_BATTERY_MAX_CHARGE_RATE] = user_input[ + CONF_BATTERY_MAX_CHARGE_RATE ] - 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 + self.updated_entry[CONF_BATTERY_MAX_DISCHARGE_RATE] = user_input[ + CONF_BATTERY_MAX_DISCHARGE_RATE + ] + self.updated_entry[CONF_BATTERY_EFFICIENCY] = user_input[ + CONF_BATTERY_EFFICIENCY + ] + self.hass.config_entries.async_update_entry( + self.config_entry, + data=self.updated_entry, + options=self.config_entry.options, + ) + return await self.async_step_init() + + data_schema = { + vol.Required( + CONF_BATTERY_SIZE, default=self.updated_entry[CONF_BATTERY_SIZE] + ): vol.All(vol.Coerce(float)), + vol.Required( + CONF_BATTERY_MAX_CHARGE_RATE, + default=self.updated_entry[CONF_BATTERY_MAX_CHARGE_RATE], + ): vol.All(vol.Coerce(float)), + vol.Required( + CONF_BATTERY_MAX_DISCHARGE_RATE, + default=self.updated_entry[CONF_BATTERY_MAX_DISCHARGE_RATE], + ): vol.All(vol.Coerce(float)), + vol.Required( + CONF_BATTERY_EFFICIENCY, + default=self.updated_entry[CONF_BATTERY_EFFICIENCY], + ): vol.All(vol.Coerce(float)), + } + return self.async_show_form( + step_id="main_params", data_schema=vol.Schema(data_schema) + ) + + async def async_step_input_sensors(self, user_input=None): + """Handle options flow.""" + self.current_input_entry = None + return self.async_show_menu( + step_id="input_sensors", + menu_options=[ + "add_import_meter", + "add_export_meter", + "edit_input_tariff", + "delete_input", + ], + ) + + async def async_step_delete_input(self, user_input=None): + if user_input is not None: + for input in self.updated_entry[CONF_INPUT_LIST]: + if input[SENSOR_ID] == user_input[CONF_INPUT_LIST]: + self.updated_entry[CONF_INPUT_LIST].remove(input) + self.hass.config_entries.async_update_entry( + self.config_entry, + data=self.updated_entry, + options=self.config_entry.options, ) - 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() - ), + return await self.async_step_init() + + list_of_inputs = [] + for input in self.updated_entry[CONF_INPUT_LIST]: + list_of_inputs.append(input[SENSOR_ID]) + + data_schema = { + vol.Required(CONF_INPUT_LIST): vol.In(list_of_inputs), + } + return self.async_show_form( + step_id="delete_input", data_schema=vol.Schema(data_schema) + ) + + async def async_step_edit_input_tariff(self, user_input=None): + if user_input is not None: + for input in self.updated_entry[CONF_INPUT_LIST]: + if input[SENSOR_ID] == user_input[CONF_INPUT_LIST]: + self.current_input_entry = input + return await self.async_tariff_menu() + + list_of_inputs = [] + for input in self.updated_entry[CONF_INPUT_LIST]: + list_of_inputs.append(input[SENSOR_ID]) + + data_schema = { + vol.Required(CONF_INPUT_LIST): vol.In(list_of_inputs), + } + return self.async_show_form( + step_id="edit_input_tariff", data_schema=vol.Schema(data_schema) + ) + + async def async_step_add_import_meter(self, user_input=None): + if user_input is not None: + self.current_input_entry: dict = { + SENSOR_ID: user_input[SENSOR_ID], + SENSOR_TYPE: IMPORT, + SIMULATED_SENSOR: f"simulated_{user_input[SENSOR_ID]}", } - 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) - ), + self.updated_entry[CONF_INPUT_LIST].append(self.current_input_entry) + return await self.async_tariff_menu() + + return self.async_show_form( + step_id="add_import_meter", + data_schema=vol.Schema( + { + vol.Required(SENSOR_ID): EntitySelector( + EntitySelectorConfig(device_class=SensorDeviceClass.ENERGY) + ), + } + ), + ) + + async def async_step_add_export_meter(self, user_input=None): + if user_input is not None: + self.current_input_entry: dict = { + SENSOR_ID: user_input[SENSOR_ID], + SENSOR_TYPE: EXPORT, + SIMULATED_SENSOR: f"simulated_{user_input[SENSOR_ID]}", } + self.updated_entry[CONF_INPUT_LIST].append(self.current_input_entry) + return await self.async_tariff_menu() + + return self.async_show_form( + step_id="add_export_meter", + data_schema=vol.Schema( + { + vol.Required(SENSOR_ID): EntitySelector( + EntitySelectorConfig(device_class=SensorDeviceClass.ENERGY) + ), + } + ), + ) + + async def async_tariff_menu(self, user_input=None): + return self.async_show_menu( + step_id="tariffMenu", + menu_options=["no_tariff_info", "fixed_tariff", "tariff_sensor"], + ) + + async def async_step_no_tariff_info(self, user_input=None): + self.current_input_entry[TARIFF_TYPE] = NO_TARIFF_INFO + self.hass.config_entries.async_update_entry( + self.config_entry, + data=self.updated_entry, + options=self.config_entry.options, + ) + return await self.async_step_init() + + async def async_step_fixed_tariff(self, user_input=None): + if user_input is not None: + self.current_input_entry[TARIFF_TYPE] = FIXED_TARIFF + self.current_input_entry[FIXED_TARIFF] = user_input[FIXED_TARIFF] + self.hass.config_entries.async_update_entry( + self.config_entry, + data=self.updated_entry, + options=self.config_entry.options, + ) + return await self.async_step_init() + + current_val = self.current_input_entry.get(FIXED_TARIFF, None) return self.async_show_form( - step_id="connecttariffsensors", data_schema=vol.Schema(schema) + step_id="fixed_tariff", + data_schema=vol.Schema( + { + vol.Optional(FIXED_TARIFF, default=current_val): vol.All( + vol.Coerce(float), + vol.Range(min=0, max=10), + ) + } + ), + ) + + async def async_step_tariff_sensor(self, user_input=None): + if user_input is not None: + self.current_input_entry[TARIFF_TYPE] = TARIFF_SENSOR + self.current_input_entry[TARIFF_SENSOR] = user_input[TARIFF_SENSOR] + self.hass.config_entries.async_update_entry( + self.config_entry, + data=self.updated_entry, + options=self.config_entry.options, + ) + return await self.async_step_init() + + current_val = self.current_input_entry.get(TARIFF_SENSOR, None) + + return self.async_show_form( + step_id="tariff_sensor", + data_schema=vol.Schema( + {vol.Required(TARIFF_SENSOR): EntitySelector(EntitySelectorConfig())} + ), ) + + async def async_step_all_done(self, user_input=None): + data = {"time": time.asctime()} + return self.async_create_entry(title="", data=data) diff --git a/custom_components/battery_sim/const.py b/custom_components/battery_sim/const.py index 641a0ff..f59a118 100644 --- a/custom_components/battery_sim/const.py +++ b/custom_components/battery_sim/const.py @@ -4,7 +4,7 @@ BATTERY_TYPE = "battery" -BATTERY_PLATFORMS = ["sensor", "switch", "button"] +BATTERY_PLATFORMS = ["sensor", "switch", "button", "select"] MESSAGE_TYPE_GENERAL = "BatteryResetMessage" MESSAGE_TYPE_BATTERY_RESET_IMP = "BatteryResetImportSim" @@ -18,6 +18,7 @@ YAML = "yaml" CONF_BATTERY = "battery" +CONF_INPUT_LIST = "input_list" CONF_IMPORT_SENSOR = "import_sensor" CONF_SECOND_IMPORT_SENSOR = "second_import_sensor" CONF_EXPORT_SENSOR = "export_sensor" @@ -30,6 +31,7 @@ CONF_ENERGY_IMPORT_TARIFF = "energy_import_tariff" CONF_ENERGY_EXPORT_TARIFF = "energy_export_tariff" CONF_UNIQUE_NAME = "unique_name" +CONF_UPDATE_FREQUENCY = "update_frequency" ATTR_VALUE = "value" METER_TYPE = "type_of_energy_meter" ONE_IMPORT_ONE_EXPORT_METER = "one_import_one_export" @@ -37,10 +39,15 @@ TWO_IMPORT_TWO_EXPORT_METER = "two_import_two_export" TARIFF_TYPE = "tariff_type" NO_TARIFF_INFO = "No tariff information" +TARIFF_SENSOR = "tariff_sensor" +FIXED_TARIFF = "fixed_tariff" TARIFF_SENSOR_ENTITIES = "Sensors that track tariffs" FIXED_NUMERICAL_TARIFFS = "Fixed value for tariffs" +NEXT_STEP = "next_step" +ADD_ANOTHER = "Add another" +ALL_DONE = "All done" -ATTR_SOURCE_ID = "source" +ATTR_SOURCE_ID = "sources" ATTR_STATUS = "status" PRECISION = 3 ATTR_ENERGY_SAVED = "total energy saved" @@ -58,6 +65,8 @@ ATTR_CHARGE_PERCENTAGE = "percentage" GRID_EXPORT_SIM = "simulated grid export after battery charging" GRID_IMPORT_SIM = "simulated grid import after battery discharging" +GRID_SECOND_EXPORT_SIM = "simulated second grid export after battery charging" +GRID_SECOND_IMPORT_SIM = "simulated second grid import after battery discharging" ICON_CHARGING = "mdi:battery-charging-50" ICON_DISCHARGING = "mdi:battery-50" ICON_FULL = "mdi:battery" @@ -65,10 +74,20 @@ OVERIDE_CHARGING = "force_charge" FORCE_DISCHARGE = "force_discharge" CHARGE_ONLY = "charge_only" +DISCHARGE_ONLY = "discharge_only" PAUSE_BATTERY = "pause_battery" RESET_BATTERY = "reset_battery" +DEFAULT_MODE = "default_mode" PERCENTAGE_ENERGY_IMPORT_SAVED = "percentage_import_energy_saved" BATTERY_CYCLES = "battery_cycles" +SENSOR_ID = "sensor_id" +SENSOR_TYPE = "sensor_type" +TARIFF = "tariff_sensor_or_value" +CONF_SECOND_ENERGY_IMPORT_TARIFF = "second_energy_import_tariff" +CONF_SECOND_ENERGY_EXPORT_TARIFF = "second_energy_export_tariff" +IMPORT = "Import" +EXPORT = "Export" +SIMULATED_SENSOR = "simulated_sensor" BATTERY_MODE = "Battery_mode_now" MODE_IDLE = "Idle/Paused" @@ -122,5 +141,11 @@ CONF_BATTERY_MAX_CHARGE_RATE: 3.84, CONF_BATTERY_EFFICIENCY: 0.90, }, + "Sessy": { + CONF_BATTERY_SIZE: 5.0, + CONF_BATTERY_EFFICIENCY: 0.81, + CONF_BATTERY_MAX_CHARGE_RATE: 2.2, + CONF_BATTERY_MAX_DISCHARGE_RATE: 1.7, + }, "Custom": {}, } diff --git a/custom_components/battery_sim/helpers.py b/custom_components/battery_sim/helpers.py new file mode 100644 index 0000000..065ab1a --- /dev/null +++ b/custom_components/battery_sim/helpers.py @@ -0,0 +1,84 @@ +from .const import ( + CONF_ENERGY_EXPORT_TARIFF, + CONF_ENERGY_IMPORT_TARIFF, + CONF_ENERGY_TARIFF, + CONF_EXPORT_SENSOR, + CONF_IMPORT_SENSOR, + CONF_SECOND_EXPORT_SENSOR, + CONF_SECOND_IMPORT_SENSOR, + FIXED_NUMERICAL_TARIFFS, + GRID_EXPORT_SIM, + GRID_IMPORT_SIM, + GRID_SECOND_EXPORT_SIM, + GRID_SECOND_IMPORT_SIM, + NO_TARIFF_INFO, + FIXED_TARIFF, + TARIFF_TYPE, + SENSOR_ID, + SENSOR_TYPE, + TARIFF_SENSOR, + CONF_SECOND_ENERGY_IMPORT_TARIFF, + CONF_SECOND_ENERGY_EXPORT_TARIFF, + IMPORT, + EXPORT, + SIMULATED_SENSOR, +) + +"""For backwards compatability with old configs""" + + +def generate_input_list(config): + tariff_type: str = TARIFF_SENSOR + if TARIFF_TYPE in config: + if config[TARIFF_TYPE] == NO_TARIFF_INFO: + tariff_type = NO_TARIFF_INFO + elif config[TARIFF_TYPE] == FIXED_NUMERICAL_TARIFFS: + tariff_type = FIXED_TARIFF + + inputs = [ + { + SENSOR_ID: config[CONF_IMPORT_SENSOR], + SENSOR_TYPE: IMPORT, + SIMULATED_SENSOR: GRID_IMPORT_SIM, + TARIFF_TYPE: tariff_type, + }, + { + SENSOR_ID: config[CONF_EXPORT_SENSOR], + SENSOR_TYPE: EXPORT, + SIMULATED_SENSOR: GRID_EXPORT_SIM, + TARIFF_TYPE: tariff_type, + }, + ] + if len(config.get(CONF_SECOND_IMPORT_SENSOR, "")) > 6: + inputs.append( + { + SENSOR_ID: config[CONF_SECOND_IMPORT_SENSOR], + SENSOR_TYPE: IMPORT, + SIMULATED_SENSOR: GRID_SECOND_IMPORT_SIM, + TARIFF_TYPE: tariff_type, + } + ) + if len(config.get(CONF_SECOND_EXPORT_SENSOR, "")) > 6: + inputs.append( + { + SENSOR_ID: config[CONF_SECOND_EXPORT_SENSOR], + SENSOR_TYPE: EXPORT, + SIMULATED_SENSOR: GRID_SECOND_EXPORT_SIM, + TARIFF_TYPE: tariff_type, + } + ) + + """Default sensor entities for backwards compatibility""" + if CONF_ENERGY_IMPORT_TARIFF in config: + inputs[0][tariff_type] = config[CONF_ENERGY_IMPORT_TARIFF] + elif CONF_ENERGY_TARIFF in config: + """For backwards compatibility""" + inputs[0][tariff_type] = config[CONF_ENERGY_TARIFF] + + if CONF_ENERGY_EXPORT_TARIFF in config: + inputs[1][tariff_type] = config[CONF_ENERGY_EXPORT_TARIFF] + if CONF_SECOND_ENERGY_IMPORT_TARIFF in config: + inputs[2][tariff_type] = config[CONF_SECOND_ENERGY_IMPORT_TARIFF] + if CONF_SECOND_ENERGY_EXPORT_TARIFF in config: + inputs[3][tariff_type] = config[CONF_SECOND_ENERGY_EXPORT_TARIFF] + return inputs diff --git a/custom_components/battery_sim/select.py b/custom_components/battery_sim/select.py new file mode 100644 index 0000000..d4b5fa4 --- /dev/null +++ b/custom_components/battery_sim/select.py @@ -0,0 +1,101 @@ +"""Switch Platform Device for Battery Sim.""" +import logging + +from homeassistant.components.select import SelectEntity + +from .const import ( + DOMAIN, + CONF_BATTERY, + OVERIDE_CHARGING, + PAUSE_BATTERY, + FORCE_DISCHARGE, + CHARGE_ONLY, + DISCHARGE_ONLY, + DEFAULT_MODE, + ICON_FULL, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + handle = hass.data[DOMAIN][config_entry.entry_id] # Get Handler + + battery_mode = BatteryMode(handle) + async_add_entities([battery_mode]) + + return True + + +async def async_setup_platform( + hass, configuration, async_add_entities, discovery_info=None +): + if discovery_info is None: + _LOGGER.error("This platform is only available through discovery") + return + + for conf in discovery_info: + battery = conf[CONF_BATTERY] + handle = hass.data[DOMAIN][battery] + + battery_mode = BatteryMode(handle) + async_add_entities([battery_mode]) + return True + + +class BatteryMode(SelectEntity): + """Switch to set the status of the Wiser Operation Mode (Away/Normal).""" + + def __init__(self, handle): + """Initialize the sensor.""" + self.handle = handle + self._device_name = handle._name + self._name = f"{handle._name} - Battery Mode" + self._options = [ + DEFAULT_MODE, + OVERIDE_CHARGING, + PAUSE_BATTERY, + FORCE_DISCHARGE, + CHARGE_ONLY, + DISCHARGE_ONLY, + ] + self._current_option = DEFAULT_MODE + + @property + def unique_id(self): + """Return uniqueid.""" + return self._name + + @property + def name(self): + return self._name + + @property + def device_info(self): + return { + "name": self._device_name, + "identifiers": {(DOMAIN, self._device_name)}, + } + + @property + def icon(self): + """Return icon.""" + return ICON_FULL + + @property + def current_option(self): + """Return the state of the sensor.""" + return self._current_option + + @property + def options(self): + return self._options + + async def async_select_option(self, option: str): + self._current_option = option + self.handle._battery_mode = option + for switch in self.handle._switches: + self.handle._switches[switch] = False + self.handle._switches[option] = True + self.schedule_update_ha_state(True) + return True diff --git a/custom_components/battery_sim/sensor.py b/custom_components/battery_sim/sensor.py index 64fac0d..89c05ae 100644 --- a/custom_components/battery_sim/sensor.py +++ b/custom_components/battery_sim/sensor.py @@ -3,10 +3,7 @@ import logging import homeassistant.util.dt as dt_util -from homeassistant.helpers.dispatcher import ( - dispatcher_send, - async_dispatcher_connect -) +from homeassistant.helpers.dispatcher import dispatcher_send, async_dispatcher_connect from homeassistant.components.sensor import ( SensorDeviceClass, @@ -14,8 +11,13 @@ SensorStateClass, ATTR_LAST_RESET, ) -from homeassistant.const import UnitOfEnergy, UnitOfPower from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.const import ( + STATE_UNAVAILABLE, + STATE_UNKNOWN, + UnitOfPower, + UnitOfEnergy, +) from .const import ( DOMAIN, @@ -37,8 +39,9 @@ ATTR_ENERGY_BATTERY_IN, CHARGING_RATE, DISCHARGING_RATE, - GRID_IMPORT_SIM, - GRID_EXPORT_SIM, + SENSOR_TYPE, + EXPORT, + SIMULATED_SENSOR, ICON_CHARGING, ICON_DISCHARGING, ICON_FULL, @@ -49,9 +52,8 @@ MODE_FULL, MODE_EMPTY, BATTERY_CYCLES, - MESSAGE_TYPE_BATTERY_RESET_IMP, - MESSAGE_TYPE_BATTERY_RESET_EXP, MESSAGE_TYPE_BATTERY_UPDATE, + SENSOR_ID, ) _LOGGER = logging.getLogger(__name__) @@ -109,64 +111,51 @@ async def define_sensors(hass, handle): ) sensors.append( DisplayOnlySensor( - handle, - CHARGING_RATE, - SensorDeviceClass.POWER, - UnitOfPower.KILO_WATT + handle, CHARGING_RATE, SensorDeviceClass.POWER, UnitOfPower.KILO_WATT ) ) sensors.append( DisplayOnlySensor( - handle, - DISCHARGING_RATE, - SensorDeviceClass.POWER, - UnitOfPower.KILO_WATT + handle, DISCHARGING_RATE, SensorDeviceClass.POWER, UnitOfPower.KILO_WATT ) ) + for input in handle._inputs: + sensors.append( + DisplayOnlySensor( + handle, + input[SIMULATED_SENSOR], + SensorDeviceClass.ENERGY, + UnitOfEnergy.KILO_WATT_HOUR, + ) + ) + + sensors.append(DisplayOnlySensor(handle, BATTERY_CYCLES, None, None)) + sensors.append( DisplayOnlySensor( handle, - GRID_EXPORT_SIM, - SensorDeviceClass.ENERGY, - UnitOfEnergy.KILO_WATT_HOUR, + ATTR_MONEY_SAVED_IMPORT, + SensorDeviceClass.MONETARY, + hass.config.currency, ) ) sensors.append( DisplayOnlySensor( handle, - GRID_IMPORT_SIM, - SensorDeviceClass.ENERGY, - UnitOfEnergy.KILO_WATT_HOUR, + ATTR_MONEY_SAVED, + SensorDeviceClass.MONETARY, + hass.config.currency, ) ) - sensors.append(DisplayOnlySensor(handle, BATTERY_CYCLES, None, None)) - if handle._import_tariff_sensor_id is not 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, - ) - ) - if handle._export_tariff_sensor_id is not None: - sensors.append( - DisplayOnlySensor( - handle, - ATTR_MONEY_SAVED_EXPORT, - SensorDeviceClass.MONETARY, - hass.config.currency, - ) + sensors.append( + DisplayOnlySensor( + handle, + ATTR_MONEY_SAVED_EXPORT, + SensorDeviceClass.MONETARY, + hass.config.currency, ) + ) sensors.append(SimulatedBattery(handle)) sensors.append(BatteryStatus(handle, BATTERY_MODE)) return sensors @@ -198,44 +187,26 @@ async def async_added_to_hass(self): await super().async_added_to_hass() state = await self.async_get_last_state() + if state: try: self._handle._sensors[self._sensor_type] = float(state.state) self._last_reset = dt_util.as_utc( - dt_util.parse_datetime( - state.attributes.get(ATTR_LAST_RESET) - ) + dt_util.parse_datetime(state.attributes.get(ATTR_LAST_RESET)) ) self._available = True await self.async_update_ha_state(True) except Exception: _LOGGER.debug("Sensor state not restored properly.") - if self._sensor_type == GRID_IMPORT_SIM: - dispatcher_send( - self.hass, - f"{self._device_name}-{MESSAGE_TYPE_BATTERY_RESET_IMP}", - ) - elif self._sensor_type == GRID_EXPORT_SIM: - dispatcher_send( - self.hass, - f"{self._device_name}-{MESSAGE_TYPE_BATTERY_RESET_EXP}", - ) + self._available = False else: _LOGGER.debug("No sensor state - presume new battery.") - if self._sensor_type == GRID_IMPORT_SIM: - dispatcher_send( - self.hass, - f"{self._device_name}-{MESSAGE_TYPE_BATTERY_RESET_IMP}" - ) - elif self._sensor_type == GRID_EXPORT_SIM: - dispatcher_send( - self.hass, - f"{self._device_name}-{MESSAGE_TYPE_BATTERY_RESET_EXP}" - ) + self._available = False async def async_update_state(): """Update sensor state.""" - self._available = True + if self._handle._sensors[self._sensor_type] is not None: + self._available = True await self.async_update_ha_state(True) async_dispatcher_connect( @@ -256,10 +227,7 @@ def unique_id(self): @property def device_info(self): - return { - "name": self._device_name, - "identifiers": {(DOMAIN, self._device_name)} - } + return {"name": self._device_name, "identifiers": {(DOMAIN, self._device_name)}} @property def native_value(self): @@ -288,25 +256,33 @@ def unit_of_measurement(self): def extra_state_attributes(self): """Return the state attributes of the sensor.""" state_attr = {} - 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: + for input in self._handle._inputs: + if self._sensor_type != input[SIMULATED_SENSOR]: + break + if input[SENSOR_TYPE] == EXPORT: + break + parent_sensor = input[SENSOR_ID] + if self.hass.states.get(parent_sensor) is None or self.hass.states.get( + parent_sensor + ).state in [STATE_UNAVAILABLE, STATE_UNKNOWN]: + break + real_world_value = float(self.hass.states.get(parent_sensor).state) + simulated_value = self._handle._sensors[self._sensor_type] + if real_world_value == 0: _LOGGER.warning( "Division by zero, real world: %s, simulated: %s, battery: %s", - real_world_import, - simulated_import, + real_world_value, + simulated_value, self._name, ) state_attr = {PERCENTAGE_ENERGY_IMPORT_SAVED: 0} else: - percentage_import_saved = ( - 100 * (real_world_import - simulated_import) - / real_world_import + percentage_value_saved = ( + 100 * (real_world_value - simulated_value) / real_world_value ) state_attr = { PERCENTAGE_ENERGY_IMPORT_SAVED: round( - float(percentage_import_saved), 0 + float(percentage_value_saved), 0 ) } return state_attr @@ -318,7 +294,11 @@ def icon(self): @property def state(self): """Return the state of the sensor.""" - if self._sensor_type == ATTR_MONEY_SAVED: + if self._sensor_type in [ + ATTR_MONEY_SAVED, + ATTR_MONEY_SAVED_EXPORT, + ATTR_MONEY_SAVED_IMPORT, + ]: return round(float(self._handle._sensors[self._sensor_type]), 2) else: return round(float(self._handle._sensors[self._sensor_type]), 3) @@ -335,7 +315,6 @@ def last_reset(self): @property def available(self) -> bool: """Needed to avoid spikes in energy dashboard on startup. - Return True if entity is available. """ return self._available @@ -357,7 +336,9 @@ async def async_added_to_hass(self): state = await self.async_get_last_state() if state: - self.handle._charge_state = float(state.state) + self.handle._charge_state = min( + float(state.state), self.handle._battery_size + ) if ATTR_DATE_RECORDING_STARTED in state.attributes: self.handle._date_recording_started = state.attributes[ ATTR_DATE_RECORDING_STARTED @@ -368,9 +349,7 @@ async def async_update_state(): await self.async_update_ha_state(True) async_dispatcher_connect( - self.hass, - f"{self._name}-{MESSAGE_TYPE_BATTERY_UPDATE}", - async_update_state + self.hass, f"{self._name}-{MESSAGE_TYPE_BATTERY_UPDATE}", async_update_state ) @property @@ -418,31 +397,24 @@ def unit_of_measurement(self): @property def extra_state_attributes(self): """Return the state attributes of the sensor.""" + sensor_list = "" + for input in self.handle._inputs: + sensor_list = f"{sensor_list}, {input[SENSOR_ID]}" return { - ATTR_STATUS: - self.handle._sensors[BATTERY_MODE], - ATTR_CHARGE_PERCENTAGE: - int(self.handle._charge_percentage), - ATTR_DATE_RECORDING_STARTED: - self.handle._date_recording_started, - 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), - CONF_BATTERY_MAX_CHARGE_RATE: - float(self.handle._max_charge_rate), - ATTR_SOURCE_ID: - self.handle._export_sensor_id, + ATTR_STATUS: self.handle._sensors[BATTERY_MODE], + ATTR_CHARGE_PERCENTAGE: int(self.handle._charge_percentage), + ATTR_DATE_RECORDING_STARTED: self.handle._date_recording_started, + 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), + CONF_BATTERY_MAX_CHARGE_RATE: float(self.handle._max_charge_rate), + ATTR_SOURCE_ID: sensor_list, } @property def icon(self): """Return the icon to use in the frontend.""" - if self.handle._sensors[BATTERY_MODE] in [ - MODE_CHARGING, MODE_FORCE_CHARGING - ]: + if self.handle._sensors[BATTERY_MODE] in [MODE_CHARGING, MODE_FORCE_CHARGING]: return ICON_CHARGING if self.handle._sensors[BATTERY_MODE] == MODE_FULL: return ICON_FULL @@ -494,10 +466,7 @@ def unique_id(self): @property def device_info(self): - return { - "name": self._device_name, - "identifiers": {(DOMAIN, self._device_name)} - } + return {"name": self._device_name, "identifiers": {(DOMAIN, self._device_name)}} @property def native_value(self): @@ -517,10 +486,7 @@ def extra_state_attributes(self): @property def icon(self): """Return the icon to use in the frontend.""" - if self.handle._sensors[BATTERY_MODE] in [ - MODE_CHARGING, - MODE_FORCE_CHARGING - ]: + if self.handle._sensors[BATTERY_MODE] in [MODE_CHARGING, MODE_FORCE_CHARGING]: return ICON_CHARGING if self.handle._sensors[BATTERY_MODE] == MODE_FULL: return ICON_FULL diff --git a/custom_components/battery_sim/switch.py b/custom_components/battery_sim/switch.py index 129def6..0b693e3 100644 --- a/custom_components/battery_sim/switch.py +++ b/custom_components/battery_sim/switch.py @@ -10,6 +10,7 @@ PAUSE_BATTERY, FORCE_DISCHARGE, CHARGE_ONLY, + DISCHARGE_ONLY, ) _LOGGER = logging.getLogger(__name__) @@ -35,11 +36,15 @@ "key": "charge_only_enabled", "icon": "mdi:home-import-outline", }, + { + "name": DISCHARGE_ONLY, + "key": "discharge_only_enabled", + "icon": "mdi:home-export-outline", + }, ] async def async_setup_entry(hass, config_entry, async_add_entities): - """Add the Wiser System Switch entities.""" handle = hass.data[DOMAIN][config_entry.entry_id] # Get Handler battery_switches = [ @@ -105,11 +110,6 @@ def icon(self): """Return icon.""" return self._icon - @property - def should_poll(self): - """Return the polling state.""" - return False - @property def is_on(self): """Return true if device is on.""" diff --git a/custom_components/battery_sim/translations/en.json b/custom_components/battery_sim/translations/en.json index 08abd94..b839cfe 100644 --- a/custom_components/battery_sim/translations/en.json +++ b/custom_components/battery_sim/translations/en.json @@ -13,10 +13,10 @@ "data": { "battery": "Battery model" }, - "description": "Select a battery model to simulate from the list or select custom to create one." + "description": "Select a battery model to simulate from the list or select Custom to create one." }, "custom": { - "title": "Custom battery", + "title": "Custom Battery", "data": { "unique_name": "Unique name", "size_kwh": "Battery size in kWh", @@ -26,32 +26,131 @@ }, "description": "Set the speciffications of the battery. Please consider posting details of the battery you are simulating on our github so we can add it to the template list." }, - "metertype": { - "title": "Energy meter type", + "meterMenu": { + "title": "Add Meters", + "menu_options": { + "add_import_meter": "Add import meter (measuring energy coming into home from the grid)", + "add_export_meter": "Add export meter (measuring energy leaving into home to the grid)", + "all_done": "All finished" + }, + "description": "At least one import and one export meter are required. Meters monitoring solar generation direcly shouldn't be added." + }, + "tariffMenu": { + "title": "Select Tariff Type", + "menu_options": { + "no_tariff_info": "No tariff for this meter", + "fixed_tariff": "A constant fixed number for the tariff", + "tariff_sensor": "A sensor that contains the value of a tariff varying over time" + }, + "description": "" + }, + "add_import_meter": { + "title": "Add Import Meter To Battery", "data": { - "one_import_one_export": "One import and one export meter", - "two_import_one_export": "Two import and one export meter", - "two_import_two_export": "Two import and two export meter" + "sensor_id": "Energy meter sensor" }, - "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." + "description": "Select meter sensor" + }, + "add_export_meter": { + "title": "Add Export Meter To Battery", + "data": { + "sensor_id": "Energy meter sensor" + }, + "description": "Select meter sensor" + }, + "fixed_tariff": { + "title": "Add Fixed Tariff Details", + "data": { + "fixed_tariff": "Fixed tariff value (if applcable)" + }, + "description": "" + }, + "tariff_sensor": { + "title": "Add Tariff Sensor Details", + "data": { + "tariff_sensor": "Sensor that shows current tariff (if applcable)" + }, + "description": "" + } + } + }, + "options": { + "step": { + "init": { + "title": "Battery Options", + "description": "Select parameters to amend", + "menu_options": { + "main_params": "Main Parameters", + "input_sensors": "Edit Meters/Sensors", + "all_done": "All done" + } + }, + "main_params": { + "title": "Main Battery Options", + "description": "Main Parameters", + "data": { + "size_kwh": "Battery size in kWh", + "max_discharge_rate_kw": "Maximum discharge rate in kW", + "max_charge_rate_kw": "Maximum charging rate in kW", + "efficiency": "Round trip efficiency (0 to 1)" + } + }, + "input_sensors": { + "title": "Edit Meters/Sensors", + "menu_options": { + "add_import_meter": "Add import meter (measuring energy coming into home from the grid)", + "add_export_meter": "Add export meter (measuring energy leaving home to the grid)", + "edit_input_tariff": "Edit tariff details for a meter", + "delete_input": "Delete a meter" + }, + "description": "At least one import and one export meter are required. Meters monitoring solar generation directly shouldn't be used." + }, + "tariffMenu": { + "title": "Select Tariff Type", + "menu_options": { + "no_tariff_info": "No tariff for this meter", + "fixed_tariff": "A constant fixed price for the tariff", + "tariff_sensor": "A sensor that represents the value of a tariff varying over time" + }, + "description": "" + }, + "add_import_meter": { + "title": "Add Import Meter To Battery", + "data": { + "sensor_id": "Energy meter sensor" + }, + "description": "Select meter sensor" + }, + "add_export_meter": { + "title": "Add Export Meter To Battery", + "data": { + "sensor_id": "Energy meter sensor" + }, + "description": "Select meter sensor" + }, + "delete_input": { + "title": "Select Meter To Delete", + "data": {}, + "description": "" + }, + "edit_input_tariff": { + "title": "Select Meter To Edit Tariff For", + "data": {}, + "description": "" }, - "connectsensors": { - "title": "Connect battery to energy meter sensors", + "fixed_tariff": { + "title": "Add Fixed Tariff", "data": { - "import_sensor": "Import sensor/meter - energy coming into house", - "second_import_sensor": "Second import sensor/meter - e.g. night time or Economy 7", - "export_sensor": "Export sensor/meter - energy sent to grid", - "second_export_sensor": "Second export sensor/meter - e.g. night time or Economy 7" + "fixed_tariff": "Fixed tariff value" }, - "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." + "description": "" }, - "connecttariffsensors": { - "title": "Connect sensors or specify fixed value for energy tariffs", + "tariff_sensor": { + "title": "Add Tariff Sensor", "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)" + "tariff_sensor": "Sensor that shows current tariff" }, - "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." + "description": "" } } } diff --git a/custom_components/battery_sim/translations/nl.json b/custom_components/battery_sim/translations/nl.json index 589d076..0788ce2 100644 --- a/custom_components/battery_sim/translations/nl.json +++ b/custom_components/battery_sim/translations/nl.json @@ -1,56 +1,55 @@ { - "config": { - "abort": { - "already_configured": "Het apparaat is reeds geconfigureerd." - }, - "error": { - "not_float": "Moet een getal zijn" - }, - "flow_title": "Nieuwe batterijsimulatie instellen", - "step": { - "user": { - "title": "Selecteer batterij", - "data": { - "battery": "Batterij model" + "config": { + "abort": { + "already_configured": "Het apparaat is reeds geconfigureerd." }, - "description": "Selecteer het te simuleren model uit de lijst, of maak een aangepast model." - }, - "custom": { - "title": "Aangepaste batterij", - "data": { - "size_kwh": "Batterij capaciteit in kWh", - "max_discharge_rate_kw": "Maximum ontlaadcapaciteit in kW", - "max_charge_rate_kw": "Maximum laadcapaciteit in kW", - "efficiency": "AC-DC-AC efficiëntie (0 to 1)" + "error": { + "not_float": "Moet een getal zijn" }, - "description": "Stel de batterijspecificaties in." - }, - "metertype": { - "title": "Energy meter type", - "data": { - "one_import_one_export": "One import and one export meter", - "two_import_one_export": "Two import and one export meter", - "two_import_two_export": "Two import and two export meter" - }, - "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." - }, - "connectsensors": { - "title": "Connect battery to energy meter sensors", - "data": { - "import_sensor": "Import sensor/meter - binnenkomende energie van het net", - "second_import_sensor": "Second import sensor/meter - e.g. night time or Economy 7", - "export_sensor": "Export sensor/meter - energy sent to grid" - }, - "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." - }, - "connecttariffsensors": { - "title": "Connect sensors for energy tariffs", - "data": { - "energy_import_tariff": "Energietarief entiteit - 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)." - } + "flow_title": "Nieuwe batterijsimulatie instellen", + "step": { + "user": { + "title": "Selecteer batterij", + "data":{ + "battery": "Batterij model" + }, + "description": "Selecteer het te simuleren model uit de lijst, of maak een aangepast model." + }, + "custom": { + "title": "Aangepaste batterij", + "data":{ + "size_kwh": "Batterij capaciteit in kWh", + "max_discharge_rate_kw" : "Maximum ontlaadcapaciteit in kW", + "max_charge_rate_kw": "Maximum laadcapaciteit in kW", + "efficiency": "AC-DC-AC efficiëntie (0 to 1)" + }, + "description": "Stel de batterijspecificaties in." + }, + "metertype":{ + "title": "Energy meter type", + "data":{ + "one_import_one_export" : "One import and one export meter", + "two_import_one_export" : "Two import and one export meter" + }, + "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." + }, + "connectsensors": { + "title": "Connect battery to energy meter sensors", + "data":{ + "import_sensor": "Import sensor/meter - binnenkomende energie van het net", + "second_import_sensor": "Second import sensor/meter - e.g. night time or Economy 7", + "export_sensor" : "Export sensor/meter - energy sent to grid" + }, + "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." + }, + "connecttariffsensors": { + "title": "Connect sensors for energy tariffs", + "data":{ + "energy_import_tariff": "Energietarief entiteit - 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)." + } + } } - } } diff --git a/custom_components/battery_sim/translations/sv.json b/custom_components/battery_sim/translations/sv.json index ef7f3ff..71277c6 100644 --- a/custom_components/battery_sim/translations/sv.json +++ b/custom_components/battery_sim/translations/sv.json @@ -1,56 +1,55 @@ { - "config": { - "abort": { - "already_configured": "Den här enheten är redan konfigurerad." - }, - "error": { - "not_float": "Måste vara en siffra" - }, - "flow_title": "Ställ in nytt simulerat batteri", - "step": { - "user": { - "title": "Välj batteri", - "data": { - "battery": "Batterimodell" + "config": { + "abort": { + "already_configured": "Den här enheten är redan konfigurerad." }, - "description": "Välj en batterimodell att simulera från listan eller välj anpassad för att skapa en ny." - }, - "custom": { - "title": "Anpassat batteri", - "data": { - "size_kwh": "Batteristorlek i kWh", - "max_discharge_rate_kw": "Max urladdningshastighet i kW", - "max_charge_rate_kw": "Max laddningshastighet i kW", - "efficiency": "Round trip efficiency (0 to 1)" + "error": { + "not_float": "Måste vara en siffra" }, - "description": "Ställ in specifikationerna för batteriet." - }, - "metertype": { - "title": "Energy meter type", - "data": { - "one_import_one_export": "One import and one export meter", - "two_import_one_export": "Two import and one export meter", - "two_import_two_export": "Two import and two export meter" - }, - "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." - }, - "connectsensors": { - "title": "Anslut batteri eller energi mätarare", - "data": { - "import_sensor": "Importera senor/mätare - inkommande energi", - "second_import_sensor": "Second import sensor/meter - e.g. night time or Economy 7", - "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." - }, - "connecttariffsensors": { - "title": "Connect sensors 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)." - } + "flow_title": "Ställ in nytt simulerat batteri", + "step": { + "user": { + "title": "Välj batteri", + "data":{ + "battery": "Batterimodell" + }, + "description": "Välj en batterimodell att simulera från listan eller välj anpassad för att skapa en ny." + }, + "custom": { + "title": "Anpassat batteri", + "data":{ + "size_kwh": "Batteristorlek i kWh", + "max_discharge_rate_kw" : "Max urladdningshastighet i kW", + "max_charge_rate_kw": "Max laddningshastighet i kW", + "efficiency": "Round trip efficiency (0 to 1)" + }, + "description": "Ställ in specifikationerna för batteriet." + }, + "metertype":{ + "title": "Energy meter type", + "data":{ + "one_import_one_export" : "One import and one export meter", + "two_import_one_export" : "Two import and one export meter" + }, + "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." + }, + "connectsensors": { + "title": "Anslut batteri eller energi mätarare", + "data":{ + "import_sensor": "Importera senor/mätare - inkommande energi", + "second_import_sensor": "Second import sensor/meter - e.g. night time or Economy 7", + "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." + }, + "connecttariffsensors": { + "title": "Connect sensors 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)." + } + } } - } } From 1eb1a8cac7cdea7d00c715c05217a8ece7176fe8 Mon Sep 17 00:00:00 2001 From: hif2k1 Date: Mon, 4 Dec 2023 21:06:38 +0000 Subject: [PATCH 2/3] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index dc619ed..63e65dd 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ Allows you to model how much energy you would save with a home battery if you currently export energy to the grid e.g. from solar panels. Requires an energy monitor that can measure import and export energy. Whenever you are exporting energy your simulated battery will charge up and whenevery you are importing it will discharge. Battery charge percentage and total energy saved are in the attributes. +Please note this is a simulation and a real battery may behave differently and not all batteries will support all the features available in this simulation. In particular battery_sim allows you to simulate batteries that charge and discharge across multiple phases and various modes including charge_only, discharge_only etc that may not be available in all real world batteries. + ## Setup The easiest way to get battery_sim is to use HACS to add it as an integration. If you don't want to use HACS you can just copy the code into the custom_components folder in your home assistant config folder. From 3784c73de4939b38ea544a7f5145250789fd72a8 Mon Sep 17 00:00:00 2001 From: hif2k1 Date: Mon, 4 Dec 2023 21:27:32 +0000 Subject: [PATCH 3/3] Update __init__.py Remove debugging logging --- custom_components/battery_sim/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/custom_components/battery_sim/__init__.py b/custom_components/battery_sim/__init__.py index 6f1dfa6..8590b6d 100644 --- a/custom_components/battery_sim/__init__.py +++ b/custom_components/battery_sim/__init__.py @@ -539,9 +539,7 @@ def update_battery(self, import_amount, export_amount): self._sensors[ self._last_import_reading_sensor_data[SIMULATED_SENSOR] ] += net_import - _LOGGER.warning(f"Updating simulated export sensor: baterry: {self._name}, last_export_sensor:{self._last_export_reading_sensor_data}") if self._last_export_reading_sensor_data is not None: - _LOGGER.warning(f"Updating simulated export sensor: baterry: {self._name}, last_export_sensor:{self._last_export_reading_sensor_data}, last_export_sensor[simulated_sensor]:{self._last_export_reading_sensor_data[SIMULATED_SENSOR]}") self._sensors[ self._last_export_reading_sensor_data[SIMULATED_SENSOR] ] += net_export