Skip to content

Commit

Permalink
Enable multiple import meters and tariffs for import and export
Browse files Browse the repository at this point in the history
  • Loading branch information
Hamish Findlay committed Jan 24, 2023
1 parent 1b78776 commit 6076cfa
Show file tree
Hide file tree
Showing 7 changed files with 226 additions and 51 deletions.
65 changes: 44 additions & 21 deletions custom_components/battery_sim/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@
CONF_BATTERY_MAX_DISCHARGE_RATE,
CONF_BATTERY_MAX_CHARGE_RATE,
CONF_BATTERY_SIZE,
CONF_ENERGY_TARIFF,
CONF_ENERGY_IMPORT_TARIFF,
CONF_ENERGY_EXPORT_TARIFF,
CONF_IMPORT_SENSOR,
CONF_SECOND_IMPORT_SENSOR,
CONF_EXPORT_SENSOR,
DOMAIN,
BATTERY_PLATFORMS,
Expand All @@ -49,7 +51,9 @@
MODE_FORCE_CHARGING,
MODE_FORCE_DISCHARGING,
MODE_FULL,
MODE_EMPTY
MODE_EMPTY,
ATTR_MONEY_SAVED_IMPORT,
ATTR_MONEY_SAVED_EXPORT
)

_LOGGER = logging.getLogger(__name__)
Expand All @@ -59,7 +63,7 @@
{
vol.Required(CONF_IMPORT_SENSOR): cv.entity_id,
vol.Required(CONF_EXPORT_SENSOR): cv.entity_id,
vol.Optional(CONF_ENERGY_TARIFF): cv.entity_id,
vol.Optional(CONF_ENERGY_IMPORT_TARIFF): cv.entity_id,
vol.Optional(CONF_NAME): cv.string,
vol.Required(CONF_BATTERY_SIZE): vol.All(float),
vol.Required(CONF_BATTERY_MAX_DISCHARGE_RATE): vol.All(float),
Expand Down Expand Up @@ -126,14 +130,25 @@ def __init__(
self._hass = hass
self._import_sensor_id = config[CONF_IMPORT_SENSOR]
self._export_sensor_id = config[CONF_EXPORT_SENSOR]
if (CONF_ENERGY_TARIFF not in config or
len(config[CONF_ENERGY_TARIFF]) < 6):
self._tariff_sensor_id = None
if (CONF_SECOND_IMPORT_SENSOR not in config or
len(config[CONF_SECOND_IMPORT_SENSOR]) < 6):
self._second_import_sensor_id = None
else:
self._tariff_sensor_id = config[CONF_ENERGY_TARIFF]
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):
self._import_tariff_sensor_id = None
else:
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:
self._export_tariff_sensor_id = config[CONF_ENERGY_EXPORT_TARIFF]
self._date_recording_started = time.asctime()
self._collecting1 = None
self._collecting2 = None
self._collecting3 = None
self._charging = False
self._name = config[CONF_NAME]
self._battery_size = config[CONF_BATTERY_SIZE]
Expand Down Expand Up @@ -162,7 +177,9 @@ def __init__(
GRID_EXPORT_SIM: 0.0,
GRID_IMPORT_SIM: 0.0,
ATTR_MONEY_SAVED: 0.0,
BATTERY_MODE: MODE_IDLE
BATTERY_MODE: MODE_IDLE,
ATTR_MONEY_SAVED_IMPORT: 0.0,
ATTR_MONEY_SAVED_EXPORT: 0.0
}
self._energy_saved_today = 0.0
self._energy_saved_week = 0.0
Expand Down Expand Up @@ -215,14 +232,19 @@ def reset_export_sim_sensor(self):
def async_source_tracking(self, event):
"""Wait for source to be ready, then start."""

_LOGGER.debug("<%s> monitoring %s", self._name, self._import_sensor_id)
self._collecting1 = async_track_state_change_event(
self._hass, [self._import_sensor_id], self.async_import_reading
)
_LOGGER.debug("<%s> monitoring %s", self._name, self._export_sensor_id)
_LOGGER.debug("<%s> monitoring %s", self._name, self._import_sensor_id)
if self._second_import_sensor_id != None:
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)
self._collecting2 = async_track_state_change_event(
self._hass, [self._export_sensor_id], self.async_export_reading
)
_LOGGER.debug("<%s> monitoring %s", self._name, self._export_sensor_id)

@callback
def async_export_reading(self, event):
Expand Down Expand Up @@ -331,8 +353,6 @@ def updateBattery(self, import_amount, export_amount):
net_import = max(amount_to_charge - export_amount, 0) + import_amount
self._charging = True
self._sensors[BATTERY_MODE] = MODE_FORCE_CHARGING
if self._tariff_sensor_id is not None:
net_money_saved = -1*amount_to_charge*float(self._hass.states.get(self._tariff_sensor_id).state)
elif self._switches[FORCE_DISCHARGE]:
_LOGGER.debug("Battery (%s) forced discharging.", self._name)
amount_to_charge = 0.0
Expand All @@ -341,28 +361,33 @@ def updateBattery(self, import_amount, export_amount):
net_import = max(import_amount - amount_to_discharge, 0)
self._charging = False
self._sensors[BATTERY_MODE] = MODE_FORCE_DISCHARGING
if self._tariff_sensor_id is not None:
net_money_saved = -1*amount_to_charge*float(self._hass.states.get(self._tariff_sensor_id).state)
else:
_LOGGER.debug("Battery (%s) normal mode.", self._name)
amount_to_charge = min(export_amount, max_charge, available_capacity_to_charge)
amount_to_discharge = min(import_amount, max_discharge, available_capacity_to_discharge)
net_import = import_amount - amount_to_discharge
net_export = export_amount - amount_to_charge
if (self._tariff_sensor_id is not None and
self._hass.states.get(self._tariff_sensor_id) is not None and
self._hass.states.get(self._tariff_sensor_id).state not in [STATE_UNAVAILABLE, STATE_UNKNOWN]):
net_money_saved = amount_to_discharge*float(self._hass.states.get(self._tariff_sensor_id).state)
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:
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))

self._sensors[ATTR_ENERGY_SAVED] += amount_to_discharge
self._sensors[ATTR_ENERGY_SAVED] += import_amount - net_import
self._sensors[GRID_IMPORT_SIM] += net_import
self._sensors[GRID_EXPORT_SIM] += net_export
self._sensors[ATTR_ENERGY_BATTERY_IN] += amount_to_charge
Expand All @@ -377,8 +402,6 @@ def updateBattery(self, import_amount, export_amount):
elif self._charge_percentage >98:
self._sensors[BATTERY_MODE] = MODE_FULL

if self._tariff_sensor_id is not None:
self._sensors[ATTR_MONEY_SAVED] += net_money_saved
self._energy_saved_today += amount_to_discharge
self._energy_saved_week += amount_to_discharge
self._energy_saved_month += amount_to_discharge
Expand Down
98 changes: 81 additions & 17 deletions custom_components/battery_sim/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import logging
import voluptuous as vol
from distutils import errors
from homeassistant import config_entries
from homeassistant.data_entry_flow import FlowResult
from homeassistant.components import sensor
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT,
CONF_NAME,
Expand All @@ -17,15 +16,21 @@
CONF_BATTERY_MAX_CHARGE_RATE,
CONF_BATTERY_EFFICIENCY,
CONF_IMPORT_SENSOR,
CONF_SECOND_IMPORT_SENSOR,
CONF_EXPORT_SENSOR,
CONF_ENERGY_TARIFF,
CONF_ENERGY_IMPORT_TARIFF,
CONF_ENERGY_EXPORT_TARIFF,
SETUP_TYPE,
CONFIG_FLOW
CONFIG_FLOW,
METER_TYPE,
ONE_IMPORT_ONE_EXPORT_METER,
TWO_IMPORT_ONE_EXPORT_METER,
TARIFF_TYPE,
NO_TARIFF_INFO,
TARIFF_SENSOR_ENTITIES,
FIXED_NUMERICAL_TARIFFS
)

import voluptuous as vol
from typing import Any

_LOGGER = logging.getLogger(__name__)

@config_entries.HANDLERS.register(DOMAIN)
Expand All @@ -40,10 +45,11 @@ async def async_step_user(self, user_input):
return await self.async_step_custom()
else:
self._data = BATTERY_OPTIONS[user_input[BATTERY_TYPE]]
self._data[SETUP_TYPE] = CONFIG_FLOW
self._data[CONF_NAME]=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_connectsensors()
return await self.async_step_metertype()

battery_options_names = []
for battery in BATTERY_OPTIONS:
Expand All @@ -59,10 +65,11 @@ async def async_step_user(self, user_input):
async def async_step_custom(self, user_input = None):
if user_input is not None:
self._data = user_input
self._data[SETUP_TYPE] = CONFIG_FLOW
self._data[CONF_NAME]=DOMAIN + ": " + str(user_input[CONF_BATTERY_SIZE]) + "_kWh_battery"
await self.async_set_unique_id(self._data[CONF_NAME])
self._abort_if_unique_id_configured()
return await self.async_step_connectsensors()
return await self.async_step_metertype()
errors = {"base": "error message"}

return self.async_show_form(
Expand All @@ -75,26 +82,83 @@ async def async_step_custom(self, user_input = None):
}),
)

async def async_step_connectsensors(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()

meter_types = [ONE_IMPORT_ONE_EXPORT_METER, TWO_IMPORT_ONE_EXPORT_METER]

return self.async_show_form(
step_id="metertype",
data_schema=vol.Schema({
vol.Required(METER_TYPE): vol.In(meter_types),
}),
)

async def async_step_connectsensorsoneimport(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 CONF_ENERGY_TARIFF in user_input:
self._data[CONF_ENERGY_TARIFF] = user_input[CONF_ENERGY_TARIFF]
self._data[SETUP_TYPE] = CONFIG_FLOW
return self.async_create_entry(title=self._data["name"], data=self._data)

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="connectsensors",
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()

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),
vol.Optional(CONF_ENERGY_TARIFF): vol.In(entities),
}),
)

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)

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),
}),
)
13 changes: 12 additions & 1 deletion custom_components/battery_sim/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,22 @@

CONF_BATTERY = "battery"
CONF_IMPORT_SENSOR = "import_sensor"
CONF_SECOND_IMPORT_SENSOR = "second_import_sensor"
CONF_EXPORT_SENSOR = "export_sensor"
CONF_BATTERY_SIZE = "size_kwh"
CONF_BATTERY_MAX_DISCHARGE_RATE = "max_discharge_rate_kw"
CONF_BATTERY_MAX_CHARGE_RATE = "max_charge_rate_kw"
CONF_BATTERY_EFFICIENCY = "efficiency"
CONF_ENERGY_TARIFF = "energy_tariff"
CONF_ENERGY_IMPORT_TARIFF = "energy_import_tariff"
CONF_ENERGY_EXPORT_TARIFF = "energy_export_tariff"
ATTR_VALUE = "value"
METER_TYPE = "type_of_energy_meter"
ONE_IMPORT_ONE_EXPORT_METER = "one_import_one_export"
TWO_IMPORT_ONE_EXPORT_METER = "two_import_one_export"
TARIFF_TYPE = "tariff_type"
NO_TARIFF_INFO = "No tariff information"
TARIFF_SENSOR_ENTITIES = "Sensors that track tariffs"
FIXED_NUMERICAL_TARIFFS = "Fixed value for tariffs"

ATTR_SOURCE_ID = "source"
ATTR_STATUS = "status"
Expand All @@ -43,6 +52,8 @@
ATTR_ENERGY_BATTERY_OUT = "battery_energy_out"
ATTR_ENERGY_BATTERY_IN = "battery_energy_in"
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"
Expand Down
9 changes: 7 additions & 2 deletions custom_components/battery_sim/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
CONF_BATTERY_MAX_CHARGE_RATE,
CONF_BATTERY_SIZE,
ATTR_MONEY_SAVED,
ATTR_MONEY_SAVED_IMPORT,
ATTR_MONEY_SAVED_EXPORT,
ATTR_SOURCE_ID,
ATTR_STATUS,
ATTR_ENERGY_SAVED,
Expand Down Expand Up @@ -75,8 +77,11 @@ 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))
if handle._tariff_sensor_id != "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))
if handle._export_tariff_sensor_id != None:
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
Expand Down Expand Up @@ -434,4 +439,4 @@ def state(self):
return self.handle._sensors[BATTERY_MODE]

def update(self):
"""Not used"""
"""Not used"""
Loading

0 comments on commit 6076cfa

Please sign in to comment.