[
  {
    "path": ".github/dependabot.yml",
    "content": "# Set update schedule for GitHub Actions\nversion: 2\nupdates:\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n"
  },
  {
    "path": ".github/workflows/hacsaction.yml",
    "content": "name: HACS Action\n\non:\n  push:\n  pull_request:\n  schedule:\n    - cron: \"0 0 * * *\"\n\njobs:\n  hacs:\n    name: HACS Action\n    runs-on: \"ubuntu-latest\"\n    steps:\n      - uses: \"actions/checkout@v6\"\n      - name: HACS Action\n        uses: \"hacs/action@main\"\n        with:\n          category: \"integration\"\n"
  },
  {
    "path": ".github/workflows/main.yml",
    "content": "name: Validate with hassfest\n\non:\n  push:\n  pull_request:\n\njobs:\n  validate:\n    runs-on: \"ubuntu-latest\"\n    steps:\n        - uses: \"actions/checkout@v6\"\n        - uses: \"home-assistant/actions/hassfest@master\"\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store"
  },
  {
    "path": "README.md",
    "content": "# Battery Simulator integration/custom component for home assistant\n\n[![downloads](https://img.shields.io/badge/dynamic/json?color=41BDF5&logo=home-assistant&label=integration%20usage&suffix=%20installs&cacheSeconds=15600&url=https://analytics.home-assistant.io/custom_integrations.json&query=$.battery_sim.total)](https://github.com/dewi-ny-je/battery_sim/)\n\nAllows 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. \n\nPlease 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.\n\n## Setup\n\nThe easiest way to get battery_sim is to use HACS to add it as an integration. If you do not want to use HACS, copy this repository into the `custom_components` folder in your Home Assistant configuration directory.\n\nAfter installation, create one or more batteries. The recommended approach is to go to **Settings > Devices & Services**, click **Add Integration**, search for **Battery Simulation**, and work through the dialog for each battery you want to simulate.\n\nYou can also define batteries in `configuration.yaml`. Each battery is created under `battery_sim:` using a unique slug key. All YAML parameters currently supported by the integration are listed below.\n\n### YAML parameters\n\n| Parameter | Required | Description |\n| --- | --- | --- |\n| `import_sensor` | Yes | Entity ID of the cumulative energy-import sensor in kWh, for example the output of a `utility_meter`. |\n| `export_sensor` | Yes | Entity ID of the cumulative energy-export sensor in kWh. |\n| `size_kwh` | Yes | Usable battery capacity in kWh. Use a floating-point value such as `13.5`. |\n| `max_discharge_rate_kw` | Yes | Maximum rated discharge power in kW. Use a floating-point value such as `5.0`. The user can limit if further using a field in the device page. |\n| `max_charge_rate_kw` | No | Maximum rated charge power in kW. Defaults to `1.0` if omitted.  The user can limit if further using a field in the device page. |\n| `discharge_efficiency` | No | Battery discharge efficiency from `0` to `1`. If omitted, the integration falls back to `efficiency` when that legacy key is present, otherwise `1.0`. You can enter either a single value between 0 and 1, or a power curve such as `0:0.90, 2.5:0.94, 5:0.95`. |\n| `charge_efficiency` | No | Battery charge efficiency from `0` to `1`. Defaults to `1.0` if omitted. You can enter either a single value between 0 and 1, or a power curve such as `0:0.90, 2.5:0.94, 5:0.95`. |\n| `efficiency` | No | Legacy single-value efficiency key kept for backward compatibility. It is used as the default for `discharge_efficiency` when the newer split efficiency keys are not set. |\n| `energy_tariff` | No | Entity ID of a tariff sensor. For backward-compatible YAML setups this populates the import tariff input. |\n| `energy_import_tariff` | No | Entity ID of an import tariff sensor. |\n| `energy_export_tariff` | No | Entity ID of an export tariff sensor. |\n| `solar_energy_sensor` | No | Entity ID of a cumulative solar energy production sensor in kWh. When configured, the maximum charge power is capped by the solar production rate during each update interval. Seldomly needed, see below. |\n| `nominal_inverter_power_kw` | No | Nominal inverter AC power limit in kW. Used together with `solar_energy_sensor` to cap battery discharge to `max(0, nominal_inverter_power_kw - current_solar_power_kw)` each update interval. |\n| `name` | No | Friendly name shown in Home Assistant. If omitted, the YAML object key is used. |\n| `rated_battery_cycles` | No | Number of full cycles at which end-of-life degradation is reached. Defaults to `6000`. |\n| `end_of_life_degradation` | No | Remaining usable capacity at `rated_battery_cycles`, expressed from `0` to `1`. Defaults to `0.8`. |\n| `update_frequency` | No | Maximum interval between updates in seconds. Defaults to `60`, which is also the recommended value. Faster updates do not improve accuracy. |\n\n### Example YAML\n\n```yaml\nbattery_sim:\n  tesla_powerwall:\n    name: Tesla Powerwall\n    import_sensor: sensor.circuitsetup_cumulative_import_energy_kwh\n    export_sensor: sensor.circuitsetup_cumulative_export_energy_kwh\n    size_kwh: 13.5\n    max_discharge_rate_kw: 5.0\n    max_charge_rate_kw: 3.68\n    discharge_efficiency: 0:0.92, 2.5:0.95, 5:0.95\n    charge_efficiency: 0:0.90, 2:0.94, 3.68:0.95\n    solar_energy_sensor: sensor.solar_generation_energy_kwh\n    nominal_inverter_power_kw: 5.0\n    rated_battery_cycles: 6000\n    end_of_life_degradation: 0.8\n    update_frequency: 60\n    energy_tariff: sensor.energy_tariff\n  lg_chem_resu10h:\n    name: LG Chem\n    import_sensor: sensor.circuitsetup_cumulative_import_energy_kwh\n    export_sensor: sensor.circuitsetup_cumulative_export_energy_kwh\n    size_kwh: 9.3\n    max_discharge_rate_kw: 5.0\n    max_charge_rate_kw: 3.3\n    discharge_efficiency: 0.975\n    charge_efficiency: 0.975\n    energy_import_tariff: sensor.grid_import_tariff\n    energy_export_tariff: sensor.grid_export_tariff\n```\n\n## Sensors\n\nThe integration creates the following sensors for each battery:\n\n| Sensor | Description | Unit |\n| --- | --- | --- |\n| `current charging rate` | Real-time charging power based on the energy transferred during the last update interval. | kW |\n| `current discharging rate` | Real-time discharging power based on the energy transferred during the last update interval. | kW |\n| `solar power cap` | Average power corresponding to the solar generation cap, updated each interval. Only available when a solar energy sensor is configured. | kW |\n| `battery_energy_in` | Cumulative energy charged into the battery since initialization or last reset. | kWh |\n| `battery_energy_out` | Cumulative energy discharged from the battery since initialization or last reset. | kWh |\n| `total energy saved` | Total energy saved compared to direct grid use. | kWh |\n| `total_money_saved` | Total money saved by the battery operation. | Currency |\n| `money_saved_on_imports` | Money saved by reducing energy imports from the grid. | Currency |\n| `extra_money_earned_on_exports` | Extra revenue earned by exporting energy to the grid. | Currency |\n| `last charge efficiency` | Charge efficiency used in the most recent update. | Ratio |\n| `last discharge efficiency` | Discharge efficiency used in the most recent update. | Ratio |\n| `battery_cycles` | Number of full charge/discharge cycles accumulated. | Cycles |\n| `battery_degradation` | Current degradation factor (1.0 = no degradation). | Ratio |\n| `Battery_mode_now` | Current operating mode (Charging, Discharging, Idle, etc.). | State |\n| `percentage` | Current charge level as a percentage. | % |\n| `status` | Status indicator showing if battery is Full, Empty, or Normal. | State |\n\n## Solar Power Cap : Important Remarks\n\nWhen a solar energy sensor is configured via the `solar_energy_sensor` parameter, the integration uses solar generation data to cap the maximum charging power during each update interval.\n\nFor this feature to work, **it is essential that the entity tracking the solar energy production gets updated more often than the battery simulator**, which is by default once per 60 seconds. If thats not the case, the battery simulator would not detect any energy generation for some of the update intervals and incorrectly pause the battery. A practical example: APsystems' microinverters communicate via ZigBee and publish power and energy updates every 5 minutes. They are therefore incompatible with this feature.\n\nIf `nominal_inverter_power_kw` is also configured, discharge is additionally capped to the inverter headroom, taking into account the solar energy sensor change over the current update interval.\n\nThis is useful only in the scenario in which batteries are connected to inverters which are \"one way\", meaning these inverters can use energy from the panels to charge the battery and use the battery to provide power to the rest of the network, but which cannot use energy from the grid to charge the batteries. \n\nThis parameter is needed also2 when there are more batteries (and inverters) than the available energy readings (which typically means two of such batteries and inverters), because if there is only one of such batteries and inverters, the only excess power seen by the smart meter is inevitably the power from the only inverter, and this parameter is not needed.\n\nIn such a scenario the simulator would not be able to know whether the excess energy (the one normally exported to the grid) come from one or the other inverter, so it would potentially use excess production from one inverter to charge a battery connected behind the other inverter. \n\nIn such an unusual scenario this parameter fixes the issue.\n\nTo be clear: do NOT set the charge cap to the smart meter energy production: it does not represent how the battery or inverter behave and it will cause undesired (and unrealistic) results.\n\n## Battery Efficiencies\n\nThis integration supports separate `charge_efficiency` and `discharge_efficiency` values because battery efficiency is not flat across the full operating range. In practice, manufacturers often publish an efficiency curve: efficiency changes with charge or discharge power, and lower power levels typically produce worse results than the headline datasheet number.\n\nYou can configure each efficiency either as:\n\n- a single value, for example `0.95`\n- or a power curve, for example `0:0.88, 0.5:0.90, 2.5:0.94, 5:0.95`\n\nThe power values are in kW. During each battery update, the integration computes the average charging or discharging power as:\n\n`energy transferred during the interval / interval duration`\n\nIt then linearly interpolates the efficiency from the configured points and uses that value for the update. Two extra sensors report the charge and discharge efficiency used for the most recent update.\n\nA simplified efficiency curve usually looks something like this:\n\n| Charge/discharge power | Typical behavior |\n| --- | --- |\n| Very low power | Efficiency drops because fixed inverter and standby losses dominate. |\n| Medium power | Efficiency is usually at or near the best point on the curve. |\n| Very high power | Efficiency can taper off again because of conversion and thermal losses. |\n\nIf you use fixed efficiencies rather than an efficiency curve, the best approach is to choose values that represent your most common operating range. If most of your simulated battery activity happens at low power, set lower `charge_efficiency` and `discharge_efficiency` values to approximate that part of the curve and choose more conservative efficiency values than the best-case number in the datasheet.\n\nWhen reading a datasheet, make sure the quoted efficiency covers the whole path you care about. Some manufacturers quote inverter efficiency only, which may describe battery-to-AC conversion while excluding charging losses into the battery. In those cases, use conservative values for both charge and discharge.\n\n## Battery Degradation\n\nThis integration models the degradation of the battery linearly, from 100% usable capacity (no degradation) at 0 cycles and (by default)\n80% usable capacity at 6000 cycles. The values can be provided in the settings.\n\nThe state of charge (SOC) is not limited progressively, the capacity associated with 100% SOC simply decreases over time.\n\nA new action is provided to manually set the current number of battery cycles, to simulate immediately old batteries.\n\n## Energy Dashboard\n\nYou can configure battery_sim to display your simulated battery on your Energy Dashboard:\n\n![Screenshot 2022-03-15 19 36 47](https://user-images.githubusercontent.com/79175134/158349586-cfc64761-0614-4067-a18a-5603d2288d7c.png)\n\n\n![image](https://user-images.githubusercontent.com/79175134/157999078-0174ab36-9f71-47c8-8585-73d6eb3acec8.png)\n\n## Debug\n\nIf you are having problems it is helpful to get the debug log for the battery by adding:\n\n```\nlogger:\n  default: critical\n  logs:\n    custom_components.battery_sim: debug\n```\n\nto your configuration.yaml and then restarting. If you leave it to run for a few minutes go to logs then and click \"load full log\" you should see entries from the battery saying it's been set up and then each time it receives an update. If you need to raise an issue then including this code is helpful.\n\n## Acknowledgements\n\nOriginal idea and integration developed by hif2k1. Further work in cooperation with dewi-ny-je.\n"
  },
  {
    "path": "custom_components/battery_sim/__init__.py",
    "content": "\"\"\"Simulates a battery to evaluate how much energy it could save.\"\"\"\nimport logging\nimport asyncio\nfrom datetime import timedelta\n\nimport voluptuous as vol\nimport homeassistant.util.dt as dt_util\n\nfrom homeassistant.core import callback\nfrom homeassistant.helpers import device_registry as dr\nfrom homeassistant.helpers import discovery\nimport homeassistant.helpers.config_validation as cv\nfrom homeassistant.helpers.start import async_at_start\nfrom homeassistant.helpers.event import (\n    async_call_later,\n    async_track_state_change_event,\n    async_track_time_interval,\n)\nfrom homeassistant.helpers.dispatcher import dispatcher_send, async_dispatcher_connect\n\nfrom homeassistant.const import (\n    ATTR_UNIT_OF_MEASUREMENT,\n    CONF_NAME,\n    STATE_UNAVAILABLE,\n    STATE_UNKNOWN,\n    UnitOfEnergy,\n)\n\nfrom .const import (\n    ATTR_ENERGY_BATTERY_IN,\n    ATTR_ENERGY_BATTERY_OUT,\n    ATTR_ENERGY_SAVED,\n    ATTR_STATUS,\n    ATTR_MONEY_SAVED_EXPORT,\n    ATTR_MONEY_SAVED_IMPORT,\n    ATTR_MONEY_SAVED,\n    ATTR_LAST_CHARGE_EFFICIENCY,\n    ATTR_LAST_DISCHARGE_EFFICIENCY,\n    BATTERY_DEGRADATION,\n    BATTERY_CYCLES,\n    BATTERY_MODE,\n    BATTERY_PLATFORMS,\n    CHARGE_ONLY,\n    CHARGING_RATE,\n    CONF_BATTERY_CHARGE_EFFICIENCY,\n    CONF_BATTERY_DISCHARGE_EFFICIENCY,\n    CONF_BATTERY_EFFICIENCY,\n    CONF_BATTERY_MAX_CHARGE_RATE,\n    CONF_BATTERY_MAX_DISCHARGE_RATE,\n    CONF_BATTERY_SIZE,\n    CONF_BATTERY,\n    CONF_END_OF_LIFE_DEGRADATION,\n    CONF_ENERGY_EXPORT_TARIFF,\n    CONF_ENERGY_IMPORT_TARIFF,\n    CONF_ENERGY_TARIFF,\n    CONF_EXPORT_SENSOR,\n    CONF_IMPORT_SENSOR,\n    CONF_SOLAR_ENERGY_SENSOR,\n    CONF_NOMINAL_INVERTER_POWER,\n    CONF_UPDATE_FREQUENCY,\n    CONF_INPUT_LIST,\n    CONF_RATED_BATTERY_CYCLES,\n    DEFAULT_MODE,\n    DISCHARGE_ONLY,\n    DISCHARGING_RATE,\n    DOMAIN,\n    FORCE_DISCHARGE,\n    MESSAGE_TYPE_BATTERY_UPDATE,\n    MESSAGE_TYPE_GENERAL,\n    MODE_CHARGING,\n    MODE_DISCHARGING,\n    MODE_EMPTY,\n    MODE_FORCE_CHARGING,\n    MODE_FORCE_DISCHARGING,\n    MODE_FULL,\n    MODE_IDLE,\n    MINIMUM_UPDATE_INTERVAL_SECONDS,\n    NO_TARIFF_INFO,\n    OVERRIDE_CHARGING,\n    PAUSE_BATTERY,\n    FIXED_TARIFF,\n    TARIFF_TYPE,\n    SENSOR_ID,\n    SENSOR_TYPE,\n    TARIFF_SENSOR,\n    IMPORT,\n    EXPORT,\n    SOLAR_POWER_CAP,\n    SIMULATED_SENSOR,\n)\nfrom .helpers import (\n    generate_input_list,\n    interpolate_efficiency,\n    parse_efficiency_curve,\n    validate_efficiency_config,\n)\n\nBATTERY_CONFIG_SCHEMA = vol.Schema(\n    vol.All(\n        {\n            vol.Required(CONF_IMPORT_SENSOR): cv.entity_id,\n            vol.Required(CONF_EXPORT_SENSOR): cv.entity_id,\n            vol.Optional(CONF_SOLAR_ENERGY_SENSOR): cv.entity_id,\n            vol.Optional(CONF_NOMINAL_INVERTER_POWER): vol.All(vol.Coerce(float), vol.Range(min=0)),\n            vol.Optional(CONF_ENERGY_TARIFF): cv.entity_id,\n            vol.Optional(CONF_ENERGY_EXPORT_TARIFF): cv.entity_id,\n            vol.Optional(CONF_ENERGY_IMPORT_TARIFF): cv.entity_id,\n            vol.Optional(CONF_NAME): cv.string,\n            vol.Required(CONF_BATTERY_SIZE): vol.All(float),\n            vol.Required(CONF_BATTERY_MAX_DISCHARGE_RATE): vol.All(float),\n            vol.Optional(CONF_BATTERY_MAX_CHARGE_RATE, default=1.0): vol.All(float),\n            vol.Optional(CONF_BATTERY_DISCHARGE_EFFICIENCY): vol.Any(\n                vol.Coerce(float), vol.All(cv.string, validate_efficiency_config)\n            ),\n            vol.Optional(CONF_BATTERY_CHARGE_EFFICIENCY): vol.Any(\n                vol.Coerce(float), vol.All(cv.string, validate_efficiency_config)\n            ),\n            vol.Optional(CONF_BATTERY_EFFICIENCY, default=1.0): vol.Any(\n                vol.Coerce(float), vol.All(cv.string, validate_efficiency_config)\n            ),\n            vol.Optional(CONF_RATED_BATTERY_CYCLES, default=6000): vol.All(\n                vol.Coerce(float), vol.Range(min=1)\n            ),\n            vol.Optional(CONF_END_OF_LIFE_DEGRADATION, default=0.8): vol.All(\n                vol.Coerce(float), vol.Range(min=0, max=1)\n            ),\n            vol.Optional(CONF_UPDATE_FREQUENCY, default=60): vol.All(\n                vol.Coerce(int), vol.Range(min=1)\n            ),\n        },\n    )\n)\n\nCONFIG_SCHEMA = vol.Schema(\n    {DOMAIN: vol.Schema({cv.slug: BATTERY_CONFIG_SCHEMA})}, extra=vol.ALLOW_EXTRA\n)\n\n_LOGGER = logging.getLogger(__name__)\nSERVICE_REGISTRATION_KEY = f\"{DOMAIN}_services_registered\"\n\nINITIAL_SOC_RATIO = 0.5\nINITIAL_CHARGE_PERCENTAGE = 50\nDEFAULT_BATTERY_STATUS = \"Normal\"\nDEFAULT_BATTERY_DEGRADATION = 1.0\n\n\nasync def async_setup(hass, config):\n    \"\"\"Set up battery platforms from a YAML.\"\"\"\n    hass.data.setdefault(DOMAIN, {})\n\n    if config.get(DOMAIN) is None:\n        return True\n\n    for battery, conf in config.get(DOMAIN).items():\n        _LOGGER.debug(\"Setup %s.%s\", DOMAIN, battery)\n        handle = SimulatedBatteryHandle(conf, hass)\n        if battery in hass.data[DOMAIN]:\n            _LOGGER.warning(\"Battery name not unique - not able to create.\")\n            continue\n        hass.data[DOMAIN][battery] = handle\n\n        for platform in BATTERY_PLATFORMS:\n            hass.async_create_task(\n                discovery.async_load_platform(\n                    hass,\n                    platform,\n                    DOMAIN,\n                    [{CONF_BATTERY: battery, CONF_NAME: conf.get(CONF_NAME, battery)}],\n                    config,\n                )\n            )\n    return True\n\n\nasync def async_setup_entry(hass, entry) -> bool:\n    \"\"\"Set up battery platforms from a Config Flow Entry.\"\"\"\n    hass.data.setdefault(DOMAIN, {})\n    \n    _LOGGER.debug(\"Setup %s.%s\", DOMAIN, entry.data[CONF_NAME])\n\n    handle = SimulatedBatteryHandle(entry.data, hass, entry.entry_id)\n    hass.data[DOMAIN][entry.entry_id] = handle\n\n    # Register service\n    async def handle_set_charge(call):\n        device_id = call.data.get(\"device_id\")\n        state = call.data.get(\"charge_state\")\n        _LOGGER.debug(\"Calling set_battery_charge_state with: %s\", state)\n\n        # Lookup the device to get the correct handle\n        dev_reg = dr.async_get(hass)\n        device = dev_reg.async_get(device_id)\n        if not device:\n            _LOGGER.error(\"Device not found: %s\", device_id)\n            return\n\n        # Match to correct handle by comparing identifiers\n        for handle_entry in hass.data[DOMAIN].values():\n            if handle_entry.matches_device_identifiers(device.identifiers):\n                handle_entry.async_set_battery_charge_state(state)\n                _LOGGER.debug(\"Battery charge updated for device %s\", handle_entry._name)\n                break\n        else:\n            _LOGGER.error(\"No handle matched for device_id: %s\", device_id)\n\n    async def handle_set_cycles(call):\n        device_id = call.data.get(\"device_id\")\n        cycles = call.data.get(\"battery_cycles\")\n        _LOGGER.debug(\"Calling set_battery_cycles with: %s\", cycles)\n\n        dev_reg = dr.async_get(hass)\n        device = dev_reg.async_get(device_id)\n        if not device:\n            _LOGGER.error(\"Device not found: %s\", device_id)\n            return\n\n        for handle_entry in hass.data[DOMAIN].values():\n            if handle_entry.matches_device_identifiers(device.identifiers):\n                handle_entry.async_set_battery_cycles(cycles)\n                _LOGGER.debug(\"Battery cycles updated for device %s\", handle_entry._name)\n                break\n        else:\n            _LOGGER.error(\"No handle matched for device_id: %s\", device_id)\n\n    if not hass.data.get(SERVICE_REGISTRATION_KEY):\n        hass.services.async_register(\n            DOMAIN,\n            \"set_battery_charge_state\",\n            handle_set_charge,\n            schema=vol.Schema({\n                vol.Required(\"device_id\"): str,\n                vol.Required(\"charge_state\"): vol.All(vol.Coerce(float), vol.Range(min=0))\n            }),\n        )\n\n        hass.services.async_register(\n            DOMAIN,\n            \"set_battery_cycles\",\n            handle_set_cycles,\n            schema=vol.Schema({\n                vol.Required(\"device_id\"): str,\n                vol.Required(\"battery_cycles\"): vol.All(vol.Coerce(float), vol.Range(min=0))\n            }),\n        )\n        hass.data[SERVICE_REGISTRATION_KEY] = True\n\n    handle._listeners.append(entry.add_update_listener(async_update_settings))\n\n    hass.async_create_task(\n        hass.config_entries.async_forward_entry_setups(entry, BATTERY_PLATFORMS)\n    )\n\n    return True\n\nasync def async_update_settings(hass, entry):\n    _LOGGER.warning(f\"Config change detected {entry.data[CONF_NAME]}\")\n    await hass.config_entries.async_reload(entry.entry_id)\n    return\n\n\nasync def async_unload_entry(hass, config_entry):\n    \"\"\"Unload a config entry\"\"\"\n    # Unload a config entry\n    unload_ok = all(\n        await asyncio.gather(\n            *[\n                hass.config_entries.async_forward_entry_unload(config_entry, platform)\n                for platform in BATTERY_PLATFORMS\n            ]\n        )\n    )\n\n    \"\"\"Remove listeners\"\"\"\n    handle = hass.data[DOMAIN][config_entry.entry_id]\n    for listener in handle._listeners:\n        if listener is not None:\n            outcome = listener()\n            _LOGGER.warning(f\"unloading listener: {outcome}\")\n    if handle._pending_update_cancel is not None:\n        handle._pending_update_cancel()\n        handle._pending_update_cancel = None\n\n    _LOGGER.debug(\"Unload integration\")\n    if unload_ok:\n        if DOMAIN in hass.data:\n            hass.data[DOMAIN].pop(config_entry.entry_id, None)\n        if DOMAIN in hass.data and not hass.data[DOMAIN]:\n            hass.services.async_remove(DOMAIN, \"set_battery_charge_state\")\n            hass.services.async_remove(DOMAIN, \"set_battery_cycles\")\n            hass.data.pop(SERVICE_REGISTRATION_KEY, None)\n            hass.data.pop(DOMAIN, None)\n\n    return unload_ok\n\n\nclass SimulatedBatteryHandle:\n    \"\"\"Representation of the battery itself.\"\"\"\n\n    @staticmethod\n    def _safe_curve_efficiency(curve, fallback=1.0):\n        \"\"\"Return first efficiency value from a curve, or fallback when unavailable.\"\"\"\n        if curve and isinstance(curve[0], (list, tuple)) and len(curve[0]) > 1:\n            return curve[0][1]\n        return fallback\n\n    def __init__(self, config, hass, entry_id=None):\n        \"\"\"Initialize the Battery.\"\"\"\n        self._hass = hass\n        self._entry_id = entry_id\n        self._date_recording_started = dt_util.now().isoformat()\n        self._name = config[CONF_NAME]\n        self._sensor_collection: list = []\n        self._charging: bool = False\n        self._accumulated_import_reading: float = 0.0\n        self._last_battery_update_time = dt_util.utcnow().timestamp()\n        # Periodic update cadence (seconds). Falls back to 60 for backwards compatibility.\n        self._update_frequency = config.get(CONF_UPDATE_FREQUENCY, 60)\n        self._max_discharge: float = 0.0\n\n        self._charge_limit = config[CONF_BATTERY_MAX_CHARGE_RATE]\n        self._discharge_limit = config[CONF_BATTERY_MAX_DISCHARGE_RATE]\n        self._minimum_soc: float = 0\n        self._maximum_soc: float = 100\n        self._charge_percentage: float = INITIAL_CHARGE_PERCENTAGE\n        self._charge_state: float = config[CONF_BATTERY_SIZE] * INITIAL_SOC_RATIO\n        self._accumulated_export_reading: float = 0.0\n        self._accumulated_solar_reading: float = 0.0\n        self._last_import_reading_sensor_data = None\n        self._last_export_reading_sensor_data = None\n        self._energy_saved_today: float = 0.0\n        self._energy_saved_week: float = 0.0\n        self._energy_saved_month: float = 0.0\n        self._solar_entity_id = config.get(CONF_SOLAR_ENERGY_SENSOR)\n        self._nominal_inverter_power = config.get(CONF_NOMINAL_INVERTER_POWER)\n        self._listeners = []\n        self._pending_update_cancel = None\n\n        self._battery_size = config[CONF_BATTERY_SIZE]\n        self._rated_battery_cycles = config.get(CONF_RATED_BATTERY_CYCLES, 6000.0)\n        self._end_of_life_degradation = config.get(CONF_END_OF_LIFE_DEGRADATION, 0.8)\n        if self._charge_state > self._battery_size:\n            self._charge_state = self._battery_size\n        self._max_discharge_rate = config[CONF_BATTERY_MAX_DISCHARGE_RATE]\n        self._max_charge_rate = config[CONF_BATTERY_MAX_CHARGE_RATE]\n        default_discharge_efficiency = config.get(CONF_BATTERY_EFFICIENCY, 1.0)\n        self._battery_discharge_efficiency = config.get(\n            CONF_BATTERY_DISCHARGE_EFFICIENCY, default_discharge_efficiency\n        )\n        self._battery_charge_efficiency = config.get(\n            CONF_BATTERY_CHARGE_EFFICIENCY, default_discharge_efficiency\n        )\n        self._battery_discharge_efficiency_curve = parse_efficiency_curve(\n            self._battery_discharge_efficiency\n        )\n        self._battery_charge_efficiency_curve = parse_efficiency_curve(\n            self._battery_charge_efficiency\n        )\n        if CONF_INPUT_LIST in config:\n            self._inputs = config[CONF_INPUT_LIST]\n        else:\n            \"\"\"Needed for backwards compatability\"\"\"\n            self._inputs = generate_input_list(config=config)\n\n        self._switches: dict = {\n            PAUSE_BATTERY: False,\n        }\n        self._battery_mode = DEFAULT_MODE\n\n        default_charge_efficiency = self._safe_curve_efficiency(\n            self._battery_charge_efficiency_curve\n        )\n        default_discharge_efficiency = self._safe_curve_efficiency(\n            self._battery_discharge_efficiency_curve\n        )\n        self._sensors: dict = {\n            ATTR_ENERGY_SAVED: 0.0,\n            ATTR_ENERGY_BATTERY_OUT: 0.0,\n            ATTR_ENERGY_BATTERY_IN: 0.0,\n            CHARGING_RATE: 0.0,\n            DISCHARGING_RATE: 0.0,\n            SOLAR_POWER_CAP: 0.0,\n            ATTR_MONEY_SAVED: 0.0,\n            BATTERY_MODE: MODE_IDLE,\n            ATTR_STATUS: DEFAULT_BATTERY_STATUS,\n            ATTR_MONEY_SAVED_IMPORT: 0.0,\n            ATTR_MONEY_SAVED_EXPORT: 0.0,\n            BATTERY_CYCLES: 0.0,\n            BATTERY_DEGRADATION: DEFAULT_BATTERY_DEGRADATION,\n            ATTR_LAST_CHARGE_EFFICIENCY: default_charge_efficiency,\n            ATTR_LAST_DISCHARGE_EFFICIENCY: default_discharge_efficiency,\n        }\n        for input_details in self._inputs:\n            self._sensors[input_details[SIMULATED_SENSOR]] = 0.0\n\n        async_at_start(self._hass, self.async_source_tracking)\n\n        self._listeners.append(\n            async_dispatcher_connect(\n                self._hass,\n                f\"{self._name}-{MESSAGE_TYPE_GENERAL}\",\n                self.async_reset_battery,\n            )\n        )\n\n    @property\n    def device_identifier(self):\n        \"\"\"Return a stable identifier tuple used for device registry linking.\"\"\"\n        return (DOMAIN, self._entry_id or self._name)\n\n    def matches_device_identifiers(self, identifiers):\n        \"\"\"Return true when any known identifier matches this handle.\"\"\"\n        known_identifiers = {\n            self.device_identifier,\n            (DOMAIN, self._name),  # Backward compatibility for existing devices.\n        }\n        return bool(known_identifiers.intersection(identifiers))\n\n    def async_set_battery_charge_state(self, state: float):\n        \"\"\"Reset the battery to start over.\"\"\"\n        _LOGGER.debug(\"Set battery charge state\")\n\n        if state <= 0:\n            self._charge_state = 0\n        elif state <= self.current_max_capacity:\n            self._charge_state = state\n        else:\n            self._charge_state = self.current_max_capacity\n            \n        dispatcher_send(self._hass, f\"{self._name}-{MESSAGE_TYPE_BATTERY_UPDATE}\")\n        return\n\n    def async_set_battery_cycles(self, cycles: float):\n        \"\"\"Set battery cycles to simulate ageing on demand.\"\"\"\n        self._sensors[BATTERY_CYCLES] = max(float(cycles), 0.0)\n        self._sensors[ATTR_ENERGY_BATTERY_IN] = self._sensors[BATTERY_CYCLES] * float(\n            self._battery_size\n        )\n        self._sensors[BATTERY_DEGRADATION] = self.degradation_factor\n        self._charge_state = min(float(self._charge_state), self.current_max_capacity)\n        self._charge_percentage = round(100 * self._charge_state / self.current_max_capacity)\n\n        dispatcher_send(self._hass, f\"{self._name}-{MESSAGE_TYPE_BATTERY_UPDATE}\")\n        return\n\n    def async_reset_battery(self):\n        \"\"\"Reset the battery to start over.\"\"\"\n        _LOGGER.debug(\"Reset battery\")\n        for input in self._inputs:\n            self.reset_sim_sensor(input[SIMULATED_SENSOR])\n\n        self._charge_state = self.current_max_capacity * INITIAL_SOC_RATIO\n        self._charge_percentage = INITIAL_CHARGE_PERCENTAGE\n\n        default_charge_efficiency = self._safe_curve_efficiency(\n            self._battery_charge_efficiency_curve\n        )\n        default_discharge_efficiency = self._safe_curve_efficiency(\n            self._battery_discharge_efficiency_curve\n        )\n\n        self._sensors[ATTR_ENERGY_SAVED] = 0.0\n        self._sensors[ATTR_MONEY_SAVED] = 0.0\n        self._sensors[ATTR_ENERGY_BATTERY_OUT] = 0.0\n        self._sensors[ATTR_ENERGY_BATTERY_IN] = 0.0\n        self._sensors[CHARGING_RATE] = 0.0\n        self._sensors[DISCHARGING_RATE] = 0.0\n        self._sensors[BATTERY_MODE] = MODE_IDLE\n        self._sensors[ATTR_STATUS] = DEFAULT_BATTERY_STATUS\n        self._sensors[ATTR_MONEY_SAVED_IMPORT] = 0.0\n        self._sensors[ATTR_MONEY_SAVED_EXPORT] = 0.0\n        self._sensors[BATTERY_CYCLES] = 0.0\n        self._sensors[BATTERY_DEGRADATION] = DEFAULT_BATTERY_DEGRADATION\n        self._sensors[ATTR_LAST_CHARGE_EFFICIENCY] = default_charge_efficiency\n        self._sensors[ATTR_LAST_DISCHARGE_EFFICIENCY] = default_discharge_efficiency\n        self._sensors[SOLAR_POWER_CAP] = 0.0\n        self._accumulated_solar_reading = 0.0\n\n        self._energy_saved_today = 0.0\n        self._energy_saved_week = 0.0\n        self._energy_saved_month = 0.0\n\n        self._date_recording_started = dt_util.now().isoformat()\n        dispatcher_send(self._hass, f\"{self._name}-{MESSAGE_TYPE_BATTERY_UPDATE}\")\n        return\n\n    def reset_sim_sensor(self, target_sensor_key):\n        \"\"\"Reset the Simulated Sensor.\"\"\"\n        _LOGGER.debug(f\"Reset {target_sensor_key} sim sensor\")\n\n        self._sensors[target_sensor_key] = 0.0\n\n        for input_details in self._inputs:\n            if input_details[SIMULATED_SENSOR] == target_sensor_key:\n                _LOGGER.warning(input_details[SENSOR_ID])\n                if self._hass.states.get(input_details[SENSOR_ID]).state not in [\n                    STATE_UNAVAILABLE,\n                    STATE_UNKNOWN,\n                ]:\n                    self._sensors[target_sensor_key] = float(\n                        self._hass.states.get(input_details[SENSOR_ID]).state\n                    )\n\n        dispatcher_send(self._hass, f\"{self._name}-{MESSAGE_TYPE_BATTERY_UPDATE}\")\n\n    @callback\n    def async_source_tracking(self, event):\n        \"\"\"Wait for source to be ready, then start.\"\"\"\n\n        for input_details in self._inputs:\n            \"\"\"Start tracking state changes for a sensor.\"\"\"\n            self._listeners.append(\n                async_track_state_change_event(\n                    self._hass, [input_details[SENSOR_ID]], self.async_reading_handler\n                )\n            )\n        _LOGGER.debug(f\"{self._name} monitoring {input_details[SENSOR_ID]}\")\n\n        # Track solar sensor if configured\n        if self._solar_entity_id is not None:\n            self._listeners.append(\n                async_track_state_change_event(\n                    self._hass, [self._solar_entity_id], self.async_solar_reading_handler\n                )\n            )\n            _LOGGER.debug(f\"{self._name} monitoring solar sensor {self._solar_entity_id}\")\n\n        # Also update on a fixed cadence so the battery reacts even when meters\n        # publish infrequently or when only switches/controls change.\n        self._listeners.append(\n            async_track_time_interval(\n                self._hass,\n                self.async_periodic_update,\n                timedelta(seconds=int(self._update_frequency)),\n            )\n        )\n        return\n\n    @callback\n    def async_periodic_update(self, now):\n        \"\"\"Update battery on a fixed cadence using accumulated readings.\"\"\"\n        self._async_maybe_update_battery()\n\n    @callback\n    def async_reading_handler(\n        self,\n        event,\n    ):\n        sensor_id = event.data.get(\"entity_id\")\n        for input_details in self._inputs:\n            if sensor_id == input_details[SENSOR_ID]:\n                break\n        else:\n            _LOGGER.warning(\n                f\"Error reading input sensor {sensor_id} not found in input sensors\"\n            )\n            return\n\n        \"\"\"Handle the sensor state changes for import or export.\"\"\"\n        sensor_charge_rate = (\n            DISCHARGING_RATE if input_details[SENSOR_TYPE] == IMPORT else CHARGING_RATE\n        )\n\n        old_state = event.data.get(\"old_state\")\n        new_state = event.data.get(\"new_state\")\n\n        if (\n            old_state is None\n            or sensor_id is None\n            or new_state is None\n            or old_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE]\n            or new_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE]\n        ):\n            # Incorrect Setup or Sensors are not ready\n            return\n\n        units = self._hass.states.get(sensor_id).attributes.get(\n            ATTR_UNIT_OF_MEASUREMENT\n        )\n\n        if units not in [UnitOfEnergy.KILO_WATT_HOUR, UnitOfEnergy.WATT_HOUR]:\n            _LOGGER.warning(\n                \"(%s) Unsupported energy unit '%s' for sensor %s; expected kWh or Wh. Ignoring update.\",\n                self._name,\n                units,\n                sensor_id,\n            )\n            return\n\n        conversion_factor = 1.0 if units == UnitOfEnergy.KILO_WATT_HOUR else 0.001\n        unit_of_energy = \"kWh\" if units == UnitOfEnergy.KILO_WATT_HOUR else \"Wh\"\n\n        new_state_value = float(new_state.state) * conversion_factor\n        old_state_value = float(old_state.state) * conversion_factor\n\n        if self._sensors[input_details[SIMULATED_SENSOR]] is None:\n            self._sensors[input_details[SIMULATED_SENSOR]] = old_state_value\n\n        if new_state_value == old_state_value:\n            # _LOGGER.debug(\"(%s) No change in readings .. \", self._name)\n            return\n\n        reading_variance = new_state_value - old_state_value\n\n        _LOGGER.debug(\n            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}\"\n        )\n\n        if reading_variance < 0:\n            _LOGGER.debug(\n                \"(%s) %s sensor value decreased - rebasing simulated sensor %s\",\n                self._name,\n                input_details[SENSOR_TYPE],\n                input_details[SIMULATED_SENSOR],\n            )\n            self._sensors[sensor_charge_rate] = 0\n            self._sensors[input_details[SIMULATED_SENSOR]] = new_state_value\n            dispatcher_send(self._hass, f\"{self._name}-{MESSAGE_TYPE_BATTERY_UPDATE}\")\n            return\n\n        if input_details[SENSOR_TYPE] == IMPORT:\n            self._last_import_reading_sensor_data = input_details\n            self._accumulated_import_reading += reading_variance\n\n        if input_details[SENSOR_TYPE] == EXPORT:\n            self._last_export_reading_sensor_data = input_details\n            self._accumulated_export_reading += reading_variance\n\n        # NOTE: battery updates are handled by async_periodic_update().\n        return\n\n    @callback\n    def async_solar_reading_handler(self, event):\n        \"\"\"Handle state changes for solar energy sensor.\"\"\"\n        sensor_id = event.data.get(\"entity_id\")\n        old_state = event.data.get(\"old_state\")\n        new_state = event.data.get(\"new_state\")\n\n        if (\n            old_state is None\n            or sensor_id is None\n            or new_state is None\n            or old_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE]\n            or new_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE]\n        ):\n            # Sensor not ready\n            return\n\n        units = self._hass.states.get(sensor_id).attributes.get(\n            ATTR_UNIT_OF_MEASUREMENT\n        )\n\n        if units in [UnitOfEnergy.KILO_WATT_HOUR, UnitOfEnergy.WATT_HOUR]:\n            conversion_factor = 1.0 if units == UnitOfEnergy.KILO_WATT_HOUR else 0.001\n            unit_of_energy = \"kWh\" if units == UnitOfEnergy.KILO_WATT_HOUR else \"Wh\"\n        else:\n            return\n\n        new_state_value = float(new_state.state) * conversion_factor\n        old_state_value = float(old_state.state) * conversion_factor\n\n        if new_state_value == old_state_value:\n            return\n\n        reading_variance = new_state_value - old_state_value\n\n        _LOGGER.debug(\n            f\"({self._name}) Solar sensor {sensor_id}: {old_state_value} {unit_of_energy} => {new_state_value} {unit_of_energy} = Δ {reading_variance} {unit_of_energy}\"\n        )\n\n        if reading_variance < 0:\n            _LOGGER.debug(\n                \"(%s) Solar sensor value decreased - meter may have been reset\",\n                self._name,\n            )\n            self._accumulated_solar_reading = 0\n            return\n\n        self._accumulated_solar_reading += reading_variance\n\n        # NOTE: battery updates are handled by async_periodic_update().\n        return\n\n    def get_tariff_information(self, input_details):\n        if input_details is None:\n            return None\n        \"\"\"Get Tarrif information to be used for calculating.\"\"\"\n        if input_details[TARIFF_TYPE] == NO_TARIFF_INFO:\n            return None\n        elif input_details[TARIFF_TYPE] == FIXED_TARIFF:\n            return input_details[FIXED_TARIFF]\n\n        # Default behavior - assume sensor entities\n        if (\n            TARIFF_SENSOR not in input_details\n            or input_details[TARIFF_SENSOR] is None\n            or len(input_details[TARIFF_SENSOR]) < 6\n            or self._hass.states.get(input_details[TARIFF_SENSOR]) is None\n            or self._hass.states.get(input_details[TARIFF_SENSOR]).state\n            in [STATE_UNAVAILABLE, STATE_UNKNOWN]\n        ):\n            return None\n\n        return float(self._hass.states.get(input_details[TARIFF_SENSOR]).state)\n\n    def set_slider_limit(self, value: float, key: str):\n        \"\"\"Called by slider to update internal charge limit.\"\"\"\n        if key == \"charge_limit\":        \n            self._charge_limit = value\n        elif key == \"discharge_limit\":        \n            self._discharge_limit = value\n        elif key == \"minimum_soc\":        \n            self._minimum_soc = value\n        elif key == \"maximum_soc\":        \n            self._maximum_soc = value\n        else:\n            _LOGGER.error(\"Unknown slider type in __init__.py\")\n\n    @callback\n    def async_trigger_update(self):\n        \"\"\"Apply pending readings and current controls immediately.\"\"\"\n        self._async_maybe_update_battery()\n\n    def _async_maybe_update_battery(self):\n        \"\"\"Apply pending readings once the minimum update interval has elapsed.\"\"\"\n        elapsed_seconds = dt_util.utcnow().timestamp() - self._last_battery_update_time\n        if elapsed_seconds < MINIMUM_UPDATE_INTERVAL_SECONDS:\n            delay = MINIMUM_UPDATE_INTERVAL_SECONDS - elapsed_seconds\n            if self._pending_update_cancel is None:\n                _LOGGER.debug(\n                    \"(%s) Delaying battery update by %.3f seconds to satisfy minimum interval.\",\n                    self._name,\n                    delay,\n                )\n                self._pending_update_cancel = async_call_later(\n                    self._hass, delay, self._async_delayed_update\n                )\n            return\n\n        if self._pending_update_cancel is not None:\n            self._pending_update_cancel()\n            self._pending_update_cancel = None\n\n        self.update_battery(\n            self._accumulated_import_reading,\n            self._accumulated_export_reading,\n            self._accumulated_solar_reading,\n        )\n        self._accumulated_export_reading = 0.0\n        self._accumulated_import_reading = 0.0\n        self._accumulated_solar_reading = 0.0\n\n    @callback\n    def _async_delayed_update(self, _now):\n        \"\"\"Run a delayed update created to enforce the minimum update interval.\"\"\"\n        self._pending_update_cancel = None\n        self._async_maybe_update_battery()\n\n    @property\n    def degradation_factor(self) -> float:\n        \"\"\"Return current degradation factor based on charge/discharge cycles.\"\"\"\n        cycles = float(self._sensors.get(BATTERY_CYCLES, 0.0))\n        capped_progress = min(max(cycles / float(self._rated_battery_cycles), 0.0), 1.0)\n        return 1.0 - ((1.0 - float(self._end_of_life_degradation)) * capped_progress)\n\n    @property\n    def current_max_capacity(self) -> float:\n        \"\"\"Return current degraded maximum battery capacity in kWh.\"\"\"\n        return max(float(self._battery_size) * self.degradation_factor, 0.000001)\n\n    def update_battery(self, import_amount, export_amount, solar_amount=0.0):\n        \"\"\"Update battery statistics based on the reading for Im- or Export.\"\"\"\n        amount_to_charge: float = 0.0\n        amount_to_discharge: float = 0.0\n        net_export: float = 0.0\n        net_import: float = 0.0\n\n        if self._charge_state == \"unknown\":\n            self._charge_state = 0.0\n\n        \"\"\"\n            Calculate maximum possible charge and discharge based on battery\n            specifications and time since last discharge\n        \"\"\"\n        time_now = dt_util.utcnow().timestamp()\n        time_last_update = self._last_battery_update_time\n        time_since_last_battery_update = time_now - time_last_update\n\n        _LOGGER.debug(\n            \"(%s), Size: (%s)kWh, Import: (%s), Export: (%s), Initial charge level: (%s) .... Timings: %s = Now / %s = Last Update / %s Time (sec).\",\n            self._name,\n            self._battery_size,\n            import_amount,\n            export_amount,\n            self._charge_state,\n            time_now,\n            time_last_update,\n            time_since_last_battery_update,\n        )\n\n        max_discharge = time_since_last_battery_update * (\n            self._max_discharge_rate / 3600\n        )\n        max_charge = time_since_last_battery_update * (self._max_charge_rate / 3600)\n        interval_hours = max(time_since_last_battery_update / 3600, 1 / 3600)\n        charge_limit = time_since_last_battery_update * (self._charge_limit / 3600)\n        discharge_limit = time_since_last_battery_update * (self._discharge_limit / 3600)\n\n        if self._solar_entity_id is not None:\n            solar_cap = max(float(solar_amount), 0.0)\n            max_charge = min(max_charge, solar_cap)\n            self._sensors[SOLAR_POWER_CAP] = solar_cap / interval_hours\n            if self._nominal_inverter_power is not None:\n                available_inverter_discharge_power = max(\n                    float(self._nominal_inverter_power) - self._sensors[SOLAR_POWER_CAP],\n                    0.0,\n                )\n                max_discharge = min(\n                    max_discharge, available_inverter_discharge_power * interval_hours\n                )\n            _LOGGER.debug(\n                f\"({self._name}) Solar cap: {solar_cap} kWh over {interval_hours:.4f} hours = {self._sensors[SOLAR_POWER_CAP]:.3f} kW\"\n            )\n        else:\n            self._sensors[SOLAR_POWER_CAP] = 0.0\n\n        effective_max_capacity = self.current_max_capacity\n        max_charge_soc_capacity = effective_max_capacity * float(self._maximum_soc) / 100\n        min_discharge_soc_capacity = (\n            effective_max_capacity * float(self._minimum_soc) / 100\n        )\n\n        available_capacity_to_charge = max(\n            max_charge_soc_capacity - float(self._charge_state), 0\n        )\n        available_capacity_to_discharge = max(\n            float(self._charge_state) - min_discharge_soc_capacity, 0\n        )\n        \n        if self._switches[PAUSE_BATTERY] or self._battery_mode == PAUSE_BATTERY:\n            _LOGGER.debug(\"(%s) Battery paused.\", self._name)\n            amount_to_charge = 0.0\n            amount_to_discharge = 0.0\n\n        elif self._battery_mode == OVERRIDE_CHARGING:\n            _LOGGER.debug(\"(%s) Battery override charging.\", self._name)\n            amount_to_charge = min(max_charge, charge_limit)\n            amount_to_discharge = 0.0\n            self._charging = True\n\n        elif self._battery_mode == FORCE_DISCHARGE:\n            _LOGGER.debug(\"(%s) Battery forced discharging.\", self._name)\n            amount_to_charge = 0.0\n            amount_to_discharge = min(max_discharge, discharge_limit)\n\n        elif self._battery_mode == CHARGE_ONLY:\n            _LOGGER.debug(\"(%s) Battery charge only mode.\", self._name)\n            amount_to_charge = min(export_amount, max_charge, charge_limit)\n            amount_to_discharge = 0.0\n\n        elif self._battery_mode == DISCHARGE_ONLY:\n            _LOGGER.debug(\"(%s) Battery discharge only mode.\", self._name)\n            amount_to_charge = 0.0\n            amount_to_discharge = min(import_amount, max_discharge, discharge_limit)\n\n        else:\n            _LOGGER.debug(\"(%s) Battery normal mode.\", self._name)\n\n            amount_to_charge = min(export_amount, max_charge, charge_limit)\n            amount_to_discharge = min(import_amount, max_discharge, discharge_limit)\n\n        # Keep amount_to_charge as input-side energy and amount_to_discharge as\n        # output-side delivered energy. The SoC capacities are battery-internal,\n        # so convert those limits through the efficiency curve. Because the\n        # efficiency curve is power-dependent, recompute it after each clipping\n        # step until the clipped amount and efficiency agree.\n        for _ in range(10):\n            if amount_to_charge <= 0.0:\n                break\n            charge_efficiency = interpolate_efficiency(\n                self._battery_charge_efficiency_curve,\n                amount_to_charge / interval_hours,\n            )\n            clipped_amount_to_charge = min(\n                amount_to_charge,\n                available_capacity_to_charge / max(charge_efficiency, 0.000001),\n            )\n            if abs(clipped_amount_to_charge - amount_to_charge) < 0.000001:\n                break\n            amount_to_charge = clipped_amount_to_charge\n\n        for _ in range(10):\n            if amount_to_discharge <= 0.0:\n                break\n            discharge_efficiency = interpolate_efficiency(\n                self._battery_discharge_efficiency_curve,\n                amount_to_discharge / interval_hours,\n            )\n            clipped_amount_to_discharge = min(\n                amount_to_discharge,\n                available_capacity_to_discharge * discharge_efficiency,\n            )\n            if abs(clipped_amount_to_discharge - amount_to_discharge) < 0.000001:\n                break\n            amount_to_discharge = clipped_amount_to_discharge\n\n        requested_charge_power = (\n            amount_to_charge / interval_hours if amount_to_charge > 0 else 0.0\n        )\n        requested_discharge_power = (\n            amount_to_discharge / interval_hours if amount_to_discharge > 0 else 0.0\n        )\n        charge_efficiency = interpolate_efficiency(\n            self._battery_charge_efficiency_curve, requested_charge_power\n        )\n        discharge_efficiency = interpolate_efficiency(\n            self._battery_discharge_efficiency_curve, requested_discharge_power\n        )\n        self._sensors[ATTR_LAST_CHARGE_EFFICIENCY] = (\n            charge_efficiency if amount_to_charge > 0 else None\n        )\n        self._sensors[ATTR_LAST_DISCHARGE_EFFICIENCY] = (\n            discharge_efficiency if amount_to_discharge > 0 else None\n        )\n\n        if self._switches[PAUSE_BATTERY] or self._battery_mode == PAUSE_BATTERY:\n            self._sensors[BATTERY_MODE] = MODE_IDLE\n        elif self._battery_mode == OVERRIDE_CHARGING:\n            self._sensors[BATTERY_MODE] = (\n                MODE_FORCE_CHARGING if amount_to_charge > 0.0 else MODE_IDLE\n            )\n        elif self._battery_mode == FORCE_DISCHARGE:\n            self._sensors[BATTERY_MODE] = (\n                MODE_FORCE_DISCHARGING if amount_to_discharge > 0.0 else MODE_IDLE\n            )\n        elif amount_to_charge > 0.0 and amount_to_charge >= amount_to_discharge:\n            self._sensors[BATTERY_MODE] = MODE_CHARGING\n        elif amount_to_discharge > 0.0:\n            self._sensors[BATTERY_MODE] = MODE_DISCHARGING\n        else:\n            self._sensors[BATTERY_MODE] = MODE_IDLE\n\n        # Calculate net grid import/export once, using efficiency-adjusted\n        # charge/discharge amounts.\n        if self._battery_mode == OVERRIDE_CHARGING:\n            net_export = max(export_amount - amount_to_charge, 0)\n            net_import = max(amount_to_charge - export_amount, 0) + import_amount\n        elif self._battery_mode == FORCE_DISCHARGE:\n            net_export = max(amount_to_discharge - import_amount, 0) + export_amount\n            net_import = max(import_amount - amount_to_discharge, 0)\n        elif self._battery_mode == CHARGE_ONLY:\n            net_import = import_amount\n            net_export = export_amount - amount_to_charge\n        elif self._battery_mode == DISCHARGE_ONLY:\n            net_import = import_amount - amount_to_discharge\n            net_export = export_amount\n        elif self._switches[PAUSE_BATTERY] or self._battery_mode == PAUSE_BATTERY:\n            net_export = export_amount\n            net_import = import_amount\n        else:\n            net_import = import_amount - amount_to_discharge\n            net_export = export_amount - amount_to_charge\n\n        current_import_tariff = self.get_tariff_information(\n            self._last_import_reading_sensor_data\n        )\n        current_export_tariff = self.get_tariff_information(\n            self._last_export_reading_sensor_data\n        )\n\n        if current_import_tariff is not None:\n            self._sensors[ATTR_MONEY_SAVED_IMPORT] += (\n                import_amount - net_import\n            ) * current_import_tariff\n        if current_export_tariff is not None:\n            self._sensors[ATTR_MONEY_SAVED_EXPORT] += (\n                net_export - export_amount\n            ) * current_export_tariff\n        self._sensors[ATTR_MONEY_SAVED] = (\n            self._sensors[ATTR_MONEY_SAVED_IMPORT]\n            + self._sensors[ATTR_MONEY_SAVED_EXPORT]\n        )\n\n        self._charge_state = (\n            float(self._charge_state)\n            + (amount_to_charge * charge_efficiency)\n            - (amount_to_discharge / max(discharge_efficiency, 0.000001))\n        )\n\n        self._sensors[ATTR_ENERGY_SAVED] += import_amount - net_import\n\n        if self._last_import_reading_sensor_data is not None:\n            self._sensors[\n                self._last_import_reading_sensor_data[SIMULATED_SENSOR]\n            ] += net_import\n        if self._last_export_reading_sensor_data is not None:\n            self._sensors[\n                self._last_export_reading_sensor_data[SIMULATED_SENSOR]\n            ] += net_export\n\n        self._sensors[ATTR_ENERGY_BATTERY_IN] += amount_to_charge\n        self._sensors[ATTR_ENERGY_BATTERY_OUT] += amount_to_discharge\n\n        self._sensors[CHARGING_RATE] = amount_to_charge / interval_hours\n        self._sensors[DISCHARGING_RATE] = amount_to_discharge / interval_hours\n        self._sensors[BATTERY_CYCLES] = (\n            self._sensors[ATTR_ENERGY_BATTERY_IN] / self._battery_size\n        )\n        self._sensors[BATTERY_DEGRADATION] = self.degradation_factor\n\n        self._charge_state = min(float(self._charge_state), effective_max_capacity)\n\n        self._charge_percentage = round(100 * self._charge_state / effective_max_capacity)\n\n        # Keep \"mode\" (how the battery operates) separate from capacity \"status\".\n        if self._charge_percentage < 2:\n            self._sensors[ATTR_STATUS] = MODE_EMPTY\n        elif self._charge_percentage > 98:\n            self._sensors[ATTR_STATUS] = MODE_FULL\n        else:\n            self._sensors[ATTR_STATUS] = \"Normal\"\n\n        # Reset day/week/month counters using Home Assistant's configured timezone.\n        now_local = dt_util.now()\n        last_update_local = dt_util.as_local(\n            dt_util.utc_from_timestamp(time_last_update)\n        )\n        if now_local.date() != last_update_local.date():\n            self._energy_saved_today = 0\n        if now_local.isocalendar()[:2] != last_update_local.isocalendar()[:2]:\n            self._energy_saved_week = 0\n        if (now_local.year, now_local.month) != (\n            last_update_local.year,\n            last_update_local.month,\n        ):\n            self._energy_saved_month = 0\n\n        self._last_battery_update_time = time_now\n\n        dispatcher_send(self._hass, f\"{self._name}-{MESSAGE_TYPE_BATTERY_UPDATE}\")\n\n        _LOGGER.debug(\"(%s) Battery update complete. New Charge level: (%s)\", self._name, self._charge_state)\n"
  },
  {
    "path": "custom_components/battery_sim/button.py",
    "content": "\"\"\"Switch  Platform Device for Battery Sim.\"\"\"\nimport logging\n\nfrom homeassistant.components.button import ButtonEntity\nfrom homeassistant.helpers.dispatcher import dispatcher_send\n\nfrom .const import DOMAIN, CONF_BATTERY, RESET_BATTERY, MESSAGE_TYPE_GENERAL\n\n_LOGGER = logging.getLogger(__name__)\n\nBATTERY_BUTTONS = [\n    {\n        \"name\": RESET_BATTERY,\n        \"key\": \"override_charging_enabled\",\n        \"icon\": \"mdi:fast-forward\",\n    }\n]\n\n\nasync def async_setup_entry(hass, config_entry, async_add_entities):\n    \"\"\"Add the Wiser System Switch entities.\"\"\"\n    handle = hass.data[DOMAIN][config_entry.entry_id]  # Get Handler\n\n    battery_buttons = [\n        BatteryButton(handle, button[\"name\"], button[\"key\"], button[\"icon\"])\n        for button in BATTERY_BUTTONS\n    ]\n    async_add_entities(battery_buttons)\n\n    return True\n\n\nasync def async_setup_platform(\n    hass, configuration, async_add_entities, discovery_info=None\n):\n    if discovery_info is None:\n        _LOGGER.error(\"This platform is only available through discovery\")\n        return\n\n    for conf in discovery_info:\n        battery = conf[CONF_BATTERY]\n        handle = hass.data[DOMAIN][battery]\n\n    battery_buttons = [\n        BatteryButton(handle, button[\"name\"], button[\"key\"], button[\"icon\"])\n        for button in BATTERY_BUTTONS\n    ]\n    async_add_entities(battery_buttons)\n    return True\n\n\nclass BatteryButton(ButtonEntity):\n    \"\"\"Switch to set the status of the Wiser Operation Mode (Away/Normal).\"\"\"\n\n    def __init__(self, handle, button_type, key, icon):\n        \"\"\"Initialize the sensor.\"\"\"\n        self._handle = handle\n        self._key = key\n        self._icon = icon\n        self._button_type = button_type\n        self._device_name = handle._name\n        self._device_identifier = handle.device_identifier\n        self._name = f\"{handle._name} \".replace(\"_\", \" \") + f\"{button_type}\".replace(\"_\", \" \").capitalize()\n        self._attr_unique_id = f\"{handle._name} - {button_type}\"\n        self._type = type\n\n    @property\n    def unique_id(self):\n        \"\"\"Return uniqueid.\"\"\"\n        return self._attr_unique_id\n\n    @property\n    def name(self):\n        return self._name\n\n    @property\n    def device_info(self):\n        return {\n            \"name\": self._device_name,\n            \"identifiers\": {self._device_identifier},\n        }\n\n    @property\n    def icon(self):\n        \"\"\"Return icon.\"\"\"\n        return self._icon\n\n    @property\n    def should_poll(self):\n        \"\"\"Return the polling state.\"\"\"\n        return False\n\n    async def async_press(self):\n        dispatcher_send(self.hass, f\"{self._device_name}-{MESSAGE_TYPE_GENERAL}\")\n"
  },
  {
    "path": "custom_components/battery_sim/config_flow.py",
    "content": "\"\"\"Configuration flow for the Battery.\"\"\"\nimport logging\nimport voluptuous as vol\nimport time\n\nfrom homeassistant import config_entries\nfrom homeassistant.helpers.selector import (\n    EntitySelector,\n    EntitySelectorConfig,\n    TextSelector,\n    TextSelectorConfig,\n    TextSelectorType,\n)\nfrom homeassistant.components.sensor import SensorDeviceClass\nfrom homeassistant.const import CONF_NAME\nfrom homeassistant.core import callback\nfrom .const import (\n    DOMAIN,\n    BATTERY_OPTIONS,\n    BATTERY_TYPE,\n    CONF_BATTERY_SIZE,\n    CONF_BATTERY_MAX_DISCHARGE_RATE,\n    CONF_BATTERY_MAX_CHARGE_RATE,\n    CONF_BATTERY_CHARGE_EFFICIENCY,\n    CONF_BATTERY_DISCHARGE_EFFICIENCY,\n    CONF_BATTERY_EFFICIENCY,\n    CONF_END_OF_LIFE_DEGRADATION,\n    CONF_UPDATE_FREQUENCY,\n    CONF_INPUT_LIST,\n    CONF_RATED_BATTERY_CYCLES,\n    CONF_SOLAR_ENERGY_SENSOR,\n    CONF_NOMINAL_INVERTER_POWER,\n    CONF_UNIQUE_NAME,\n    SETUP_TYPE,\n    CONFIG_FLOW,\n    TARIFF_TYPE,\n    NO_TARIFF_INFO,\n    IMPORT,\n    EXPORT,\n    SENSOR_ID,\n    SENSOR_TYPE,\n    TARIFF_SENSOR,\n    FIXED_TARIFF,\n    SIMULATED_SENSOR,\n)\nfrom .helpers import generate_input_list, validate_efficiency_config\n\n\nEFFICIENCY_TEXT_SELECTOR = TextSelector(\n    TextSelectorConfig(type=TextSelectorType.TEXT)\n)\n\n_LOGGER = logging.getLogger(__name__)\n\n\n@config_entries.HANDLERS.register(DOMAIN)\nclass BatterySetupConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):\n    \"\"\"Config flow.\"\"\"\n\n    @staticmethod\n    @callback\n    def async_get_options_flow(config_entry):\n        \"\"\"Return flow options.\"\"\"\n        return BatteryOptionsFlowHandler(config_entry)\n\n    @staticmethod\n    def _validate_efficiency_fields(user_input):\n        \"\"\"Return field errors for invalid efficiency inputs.\"\"\"\n        errors = {}\n        for key in (\n            CONF_BATTERY_DISCHARGE_EFFICIENCY,\n            CONF_BATTERY_CHARGE_EFFICIENCY,\n        ):\n            try:\n                validate_efficiency_config(user_input[key])\n            except (ValueError, TypeError):\n                errors[key] = \"invalid_input\"\n        return errors\n\n    async def async_step_user(self, user_input):\n        \"\"\"Handle a flow initialized by the user.\"\"\"\n        if user_input is not None:\n            if user_input[BATTERY_TYPE] == \"Custom\":\n                return await self.async_step_custom()\n\n            self._data = BATTERY_OPTIONS[user_input[BATTERY_TYPE]]\n            self._data[SETUP_TYPE] = CONFIG_FLOW\n            self._data[CONF_NAME] = f\"{user_input[BATTERY_TYPE]}\"\n            self._data[CONF_RATED_BATTERY_CYCLES] = 6000\n            self._data[CONF_END_OF_LIFE_DEGRADATION] = 0.8\n            self._data[CONF_UPDATE_FREQUENCY] = 60\n            await self.async_set_unique_id(self._data[CONF_NAME])\n            self._abort_if_unique_id_configured()\n            self._data[CONF_INPUT_LIST] = []\n            return await self.async_step_meter_menu()\n\n        battery_options_names = list(BATTERY_OPTIONS)\n        return self.async_show_form(\n            step_id=\"user\",\n            data_schema=vol.Schema(\n                {\n                    vol.Required(BATTERY_TYPE): vol.In(battery_options_names),\n                }\n            ),\n        )\n\n    async def async_step_custom(self, user_input=None):\n        errors = {}\n        if user_input is not None:\n            errors = self._validate_efficiency_fields(user_input)\n            if not errors:\n                self._data = user_input\n                self._data[SETUP_TYPE] = CONFIG_FLOW\n                self._data[CONF_NAME] = f\"{self._data[CONF_UNIQUE_NAME]}\"\n                self._data[CONF_INPUT_LIST] = []\n                solar_sensor = user_input.get(CONF_SOLAR_ENERGY_SENSOR)\n                if solar_sensor:\n                    self._data[CONF_SOLAR_ENERGY_SENSOR] = solar_sensor\n                else:\n                    self._data.pop(CONF_SOLAR_ENERGY_SENSOR, None)\n\n                nominal_inverter_power = user_input.get(CONF_NOMINAL_INVERTER_POWER)\n                if nominal_inverter_power is not None:\n                    self._data[CONF_NOMINAL_INVERTER_POWER] = nominal_inverter_power\n                else:\n                    self._data.pop(CONF_NOMINAL_INVERTER_POWER, None)\n                await self.async_set_unique_id(self._data[CONF_NAME])\n                self._abort_if_unique_id_configured()\n                return await self.async_step_meter_menu()\n\n        return self.async_show_form(\n            step_id=\"custom\",\n            data_schema=vol.Schema(\n                {\n                    vol.Required(CONF_UNIQUE_NAME): vol.All(str),\n                    vol.Required(CONF_BATTERY_SIZE): vol.All(vol.Coerce(float)),\n                    vol.Required(CONF_BATTERY_MAX_DISCHARGE_RATE): vol.All(\n                        vol.Coerce(float)\n                    ),\n                    vol.Required(CONF_BATTERY_MAX_CHARGE_RATE): vol.All(\n                        vol.Coerce(float)\n                    ),\n                    vol.Required(\n                        CONF_BATTERY_DISCHARGE_EFFICIENCY, default=\"0.9\"\n                    ): EFFICIENCY_TEXT_SELECTOR,\n                    vol.Required(\n                        CONF_BATTERY_CHARGE_EFFICIENCY, default=\"0.9\"\n                    ): EFFICIENCY_TEXT_SELECTOR,\n                     vol.Required(CONF_RATED_BATTERY_CYCLES, default=6000): vol.All(\n                        vol.Coerce(float), vol.Range(min=1)\n                    ),\n                    vol.Required(CONF_END_OF_LIFE_DEGRADATION, default=0.8): vol.All(\n                        vol.Coerce(float), vol.Range(min=0, max=1)\n                    ),\n                    vol.Required(CONF_UPDATE_FREQUENCY, default=60): vol.All(\n                        vol.Coerce(int), vol.Range(min=1)\n                    ),\n                    vol.Optional(CONF_SOLAR_ENERGY_SENSOR): EntitySelector(\n                        EntitySelectorConfig(device_class=SensorDeviceClass.ENERGY)\n                    ),\n                    vol.Optional(CONF_NOMINAL_INVERTER_POWER): vol.All(\n                        vol.Coerce(float), vol.Range(min=0)\n                    ),\n                }\n            ),\n            errors=errors,\n        )\n\n    async def async_step_meter_menu(self, user_input=None):\n        menu_options = [\"add_import_meter\", \"add_export_meter\"]\n        import_meter: bool = False\n        export_meter: bool = False\n        for input in self._data[CONF_INPUT_LIST]:\n            if input[SENSOR_TYPE] == IMPORT:\n                import_meter = True\n            if input[SENSOR_TYPE] == EXPORT:\n                export_meter = True\n        if import_meter and export_meter:\n            menu_options.append(\"all_done\")\n        return self.async_show_menu(step_id=\"meter_menu\", menu_options=menu_options)\n\n    async def async_step_add_import_meter(self, user_input=None):\n        if user_input is not None:\n            self.current_input_entry: dict = {\n                SENSOR_ID: user_input[SENSOR_ID],\n                SENSOR_TYPE: IMPORT,\n                SIMULATED_SENSOR: f\"simulated_{user_input[SENSOR_ID]}\",\n            }\n            return await self.async_step_tariff_menu()\n\n        return self.async_show_form(\n            step_id=\"add_import_meter\",\n            data_schema=vol.Schema(\n                {\n                    vol.Required(SENSOR_ID): EntitySelector(\n                        EntitySelectorConfig(device_class=SensorDeviceClass.ENERGY)\n                    ),\n                }\n            ),\n        )\n\n    async def async_step_add_export_meter(self, user_input=None):\n        if user_input is not None:\n            self.current_input_entry: dict = {\n                SENSOR_ID: user_input[SENSOR_ID],\n                SENSOR_TYPE: EXPORT,\n                SIMULATED_SENSOR: f\"simulated_{user_input[SENSOR_ID]}\",\n            }\n            return await self.async_step_tariff_menu()\n\n        return self.async_show_form(\n            step_id=\"add_export_meter\",\n            data_schema=vol.Schema(\n                {\n                    vol.Required(SENSOR_ID): EntitySelector(\n                        EntitySelectorConfig(device_class=SensorDeviceClass.ENERGY)\n                    ),\n                }\n            ),\n        )\n\n    async def async_step_tariff_menu(self, user_input=None):\n        return self.async_show_menu(\n            step_id=\"tariff_menu\",\n            menu_options=[\"no_tariff_info\", \"fixed_tariff\", \"tariff_sensor\"],\n        )\n\n    async def async_step_no_tariff_info(self, user_input=None):\n        self.current_input_entry[TARIFF_TYPE] = NO_TARIFF_INFO\n        self._data[CONF_INPUT_LIST].append(self.current_input_entry)\n        return await self.async_step_meter_menu()\n\n    async def async_step_fixed_tariff(self, user_input=None):\n        if user_input is not None:\n            self.current_input_entry[TARIFF_TYPE] = FIXED_TARIFF\n            self.current_input_entry[FIXED_TARIFF] = user_input[FIXED_TARIFF]\n            self._data[CONF_INPUT_LIST].append(self.current_input_entry)\n            return await self.async_step_meter_menu()\n\n        return self.async_show_form(\n            step_id=\"fixed_tariff\",\n            data_schema=vol.Schema(\n                {\n                    vol.Optional(FIXED_TARIFF): vol.All(\n                        vol.Coerce(float), vol.Range(min=0, max=100)\n                    )\n                }\n            ),\n        )\n\n    async def async_step_tariff_sensor(self, user_input=None):\n        if user_input is not None:\n            self.current_input_entry[TARIFF_TYPE] = TARIFF_SENSOR\n            self.current_input_entry[TARIFF_SENSOR] = user_input[TARIFF_SENSOR]\n            self._data[CONF_INPUT_LIST].append(self.current_input_entry)\n            return await self.async_step_meter_menu()\n\n        return self.async_show_form(\n            step_id=\"tariff_sensor\",\n            data_schema=vol.Schema(\n                {vol.Required(TARIFF_SENSOR): EntitySelector(EntitySelectorConfig())}\n            ),\n        )\n\n    async def async_step_all_done(self, user_input=None):\n        return self.async_create_entry(title=self._data[CONF_NAME], data=self._data)\n\n\nclass BatteryOptionsFlowHandler(config_entries.OptionsFlow):\n    \"\"\"Handle a option flow for battery.\"\"\"\n\n    def __init__(self, config_entry=None):\n        \"\"\"Initialize options flow.\"\"\"\n        self._config_entry_compat = config_entry\n        self.updated_entry = None\n        self.current_input_entry = None\n\n    @property\n    def _battery_config_entry(self):\n        \"\"\"Return the config entry for both old and new Home Assistant cores.\"\"\"\n        return getattr(self, \"config_entry\", None) or self._config_entry_compat\n\n    @staticmethod\n    def _validate_efficiency_fields(user_input):\n        \"\"\"Return field errors for invalid efficiency inputs.\"\"\"\n        errors = {}\n        for key in (\n            CONF_BATTERY_DISCHARGE_EFFICIENCY,\n            CONF_BATTERY_CHARGE_EFFICIENCY,\n        ):\n            try:\n                validate_efficiency_config(user_input[key])\n            except (ValueError, TypeError):\n                errors[key] = \"invalid_input\"\n        return errors\n\n    async def async_step_init(self, user_input=None):\n        \"\"\"Handle options flow.\"\"\"\n        config_entry = self._battery_config_entry\n        self.updated_entry = config_entry.data.copy()\n        self._active_config_entry = config_entry\n        if CONF_INPUT_LIST not in self.updated_entry:\n            self.updated_entry[CONF_INPUT_LIST] = generate_input_list(\n                config=self.updated_entry\n            )\n\n        return self.async_show_menu(\n            step_id=\"init\", menu_options=[\"main_params\", \"input_sensors\", \"all_done\"]\n        )\n\n    async def async_step_main_params(self, user_input=None):\n        errors = {}\n        if user_input is not None:\n            errors = self._validate_efficiency_fields(user_input)\n            if not errors:\n                self.updated_entry[CONF_BATTERY_SIZE] = user_input[CONF_BATTERY_SIZE]\n                self.updated_entry[CONF_BATTERY_MAX_CHARGE_RATE] = user_input[\n                    CONF_BATTERY_MAX_CHARGE_RATE\n                ]\n                self.updated_entry[CONF_BATTERY_MAX_DISCHARGE_RATE] = user_input[\n                    CONF_BATTERY_MAX_DISCHARGE_RATE\n                ]\n                self.updated_entry[CONF_BATTERY_DISCHARGE_EFFICIENCY] = user_input[\n                    CONF_BATTERY_DISCHARGE_EFFICIENCY\n                ]\n                self.updated_entry[CONF_BATTERY_CHARGE_EFFICIENCY] = user_input[\n                    CONF_BATTERY_CHARGE_EFFICIENCY\n                ]\n                self.updated_entry[CONF_RATED_BATTERY_CYCLES] = user_input[\n                    CONF_RATED_BATTERY_CYCLES\n                ]\n                self.updated_entry[CONF_END_OF_LIFE_DEGRADATION] = user_input[\n                    CONF_END_OF_LIFE_DEGRADATION\n                ]\n                self.updated_entry.pop(CONF_BATTERY_EFFICIENCY, None)\n                self.updated_entry[CONF_UPDATE_FREQUENCY] = user_input[\n                    CONF_UPDATE_FREQUENCY\n                ]\n                if user_input.get(CONF_SOLAR_ENERGY_SENSOR):\n                    self.updated_entry[CONF_SOLAR_ENERGY_SENSOR] = user_input[\n                        CONF_SOLAR_ENERGY_SENSOR\n                    ]\n                else:\n                    self.updated_entry.pop(CONF_SOLAR_ENERGY_SENSOR, None)\n                if user_input.get(CONF_NOMINAL_INVERTER_POWER) is not None:\n                    self.updated_entry[CONF_NOMINAL_INVERTER_POWER] = user_input[\n                        CONF_NOMINAL_INVERTER_POWER\n                    ]\n                else:\n                    self.updated_entry.pop(CONF_NOMINAL_INVERTER_POWER, None)\n                self.hass.config_entries.async_update_entry(\n                    self._active_config_entry,\n                    data=self.updated_entry,\n                    options=self._active_config_entry.options,\n                )\n                return await self.async_step_init()\n\n        data_schema = {\n            vol.Required(\n                CONF_BATTERY_SIZE, default=self.updated_entry[CONF_BATTERY_SIZE]\n            ): vol.All(vol.Coerce(float)),\n            vol.Required(\n                CONF_BATTERY_MAX_CHARGE_RATE,\n                default=self.updated_entry[CONF_BATTERY_MAX_CHARGE_RATE],\n            ): vol.All(vol.Coerce(float)),\n            vol.Required(\n                CONF_BATTERY_MAX_DISCHARGE_RATE,\n                default=self.updated_entry[CONF_BATTERY_MAX_DISCHARGE_RATE],\n            ): vol.All(vol.Coerce(float)),\n            # Use .get() so existing entries using legacy `efficiency` keep working.\n            vol.Required(\n                CONF_BATTERY_DISCHARGE_EFFICIENCY,\n                default=str(\n                    self.updated_entry.get(\n                        CONF_BATTERY_DISCHARGE_EFFICIENCY,\n                        self.updated_entry.get(CONF_BATTERY_EFFICIENCY, 0.9),\n                    )\n                ),\n            ): EFFICIENCY_TEXT_SELECTOR,\n            vol.Required(\n                CONF_BATTERY_CHARGE_EFFICIENCY,\n                default=str(\n                    self.updated_entry.get(\n                        CONF_BATTERY_CHARGE_EFFICIENCY,\n                        self.updated_entry.get(CONF_BATTERY_EFFICIENCY, 1.0),\n                    )\n                ),\n            ): EFFICIENCY_TEXT_SELECTOR,\n            vol.Required(\n                CONF_RATED_BATTERY_CYCLES,\n                default=self.updated_entry.get(CONF_RATED_BATTERY_CYCLES, 6000),\n            ): vol.All(vol.Coerce(float), vol.Range(min=1)),\n            vol.Required(\n                CONF_END_OF_LIFE_DEGRADATION,\n                default=self.updated_entry.get(CONF_END_OF_LIFE_DEGRADATION, 0.8),\n            ): vol.All(vol.Coerce(float), vol.Range(min=0, max=1)),\n            vol.Required(\n                CONF_UPDATE_FREQUENCY,\n                default=self.updated_entry.get(CONF_UPDATE_FREQUENCY, 60),\n            ): vol.All(vol.Coerce(int), vol.Range(min=1)),\n            vol.Optional(\n                CONF_SOLAR_ENERGY_SENSOR,\n                description={\n                    \"suggested_value\": self.updated_entry.get(CONF_SOLAR_ENERGY_SENSOR)\n                },\n            ): EntitySelector(\n                EntitySelectorConfig(device_class=SensorDeviceClass.ENERGY)\n            ),\n            vol.Optional(\n                CONF_NOMINAL_INVERTER_POWER,\n                default=self.updated_entry.get(CONF_NOMINAL_INVERTER_POWER),\n            ): vol.Any(None, vol.All(vol.Coerce(float), vol.Range(min=0))),\n        }\n        return self.async_show_form(\n            step_id=\"main_params\",\n            data_schema=vol.Schema(data_schema),\n            errors=errors,\n        )\n\n    async def async_step_input_sensors(self, user_input=None):\n        \"\"\"Handle options flow.\"\"\"\n        self.current_input_entry = None\n        return self.async_show_menu(\n            step_id=\"input_sensors\",\n            menu_options=[\n                \"add_import_meter\",\n                \"add_export_meter\",\n                \"edit_input_tariff\",\n                \"delete_input\",\n            ],\n        )\n\n    async def async_step_delete_input(self, user_input=None):\n        if user_input is not None:\n            for input in self.updated_entry[CONF_INPUT_LIST]:\n                if input[SENSOR_ID] == user_input[CONF_INPUT_LIST]:\n                    self.updated_entry[CONF_INPUT_LIST].remove(input)\n            self.hass.config_entries.async_update_entry(\n                self._active_config_entry,\n                data=self.updated_entry,\n                options=self._active_config_entry.options,\n            )\n            return await self.async_step_init()\n\n        list_of_inputs = []\n        for input in self.updated_entry[CONF_INPUT_LIST]:\n            list_of_inputs.append(input[SENSOR_ID])\n\n        data_schema = {\n            vol.Required(CONF_INPUT_LIST): vol.In(list_of_inputs),\n        }\n        return self.async_show_form(\n            step_id=\"delete_input\", data_schema=vol.Schema(data_schema)\n        )\n\n    async def async_step_edit_input_tariff(self, user_input=None):\n        if user_input is not None:\n            for input in self.updated_entry[CONF_INPUT_LIST]:\n                if input[SENSOR_ID] == user_input[CONF_INPUT_LIST]:\n                    self.current_input_entry = input\n            return await self.async_step_tariff_menu()\n\n        list_of_inputs = []\n        for input in self.updated_entry[CONF_INPUT_LIST]:\n            list_of_inputs.append(input[SENSOR_ID])\n\n        data_schema = {\n            vol.Required(CONF_INPUT_LIST): vol.In(list_of_inputs),\n        }\n        return self.async_show_form(\n            step_id=\"edit_input_tariff\", data_schema=vol.Schema(data_schema)\n        )\n\n    async def async_step_add_import_meter(self, user_input=None):\n        if user_input is not None:\n            self.current_input_entry: dict = {\n                SENSOR_ID: user_input[SENSOR_ID],\n                SENSOR_TYPE: IMPORT,\n                SIMULATED_SENSOR: f\"simulated_{user_input[SENSOR_ID]}\",\n            }\n            self.updated_entry[CONF_INPUT_LIST].append(self.current_input_entry)\n            return await self.async_step_tariff_menu()\n\n        return self.async_show_form(\n            step_id=\"add_import_meter\",\n            data_schema=vol.Schema(\n                {\n                    vol.Required(SENSOR_ID): EntitySelector(\n                        EntitySelectorConfig(device_class=SensorDeviceClass.ENERGY)\n                    ),\n                }\n            ),\n        )\n\n    async def async_step_add_export_meter(self, user_input=None):\n        if user_input is not None:\n            self.current_input_entry: dict = {\n                SENSOR_ID: user_input[SENSOR_ID],\n                SENSOR_TYPE: EXPORT,\n                SIMULATED_SENSOR: f\"simulated_{user_input[SENSOR_ID]}\",\n            }\n            self.updated_entry[CONF_INPUT_LIST].append(self.current_input_entry)\n            return await self.async_step_tariff_menu()\n\n        return self.async_show_form(\n            step_id=\"add_export_meter\",\n            data_schema=vol.Schema(\n                {\n                    vol.Required(SENSOR_ID): EntitySelector(\n                        EntitySelectorConfig(device_class=SensorDeviceClass.ENERGY)\n                    ),\n                }\n            ),\n        )\n\n    async def async_step_tariff_menu(self, user_input=None):\n        return self.async_show_menu(\n            step_id=\"tariff_menu\",\n            menu_options=[\"no_tariff_info\", \"fixed_tariff\", \"tariff_sensor\"],\n        )\n\n    async def async_step_no_tariff_info(self, user_input=None):\n        self.current_input_entry[TARIFF_TYPE] = NO_TARIFF_INFO\n        self.hass.config_entries.async_update_entry(\n            self._active_config_entry,\n            data=self.updated_entry,\n            options=self._active_config_entry.options,\n        )\n        return await self.async_step_init()\n\n    async def async_step_fixed_tariff(self, user_input=None):\n        if user_input is not None:\n            self.current_input_entry[TARIFF_TYPE] = FIXED_TARIFF\n            self.current_input_entry[FIXED_TARIFF] = user_input[FIXED_TARIFF]\n            self.hass.config_entries.async_update_entry(\n                self._active_config_entry,\n                data=self.updated_entry,\n                options=self._active_config_entry.options,\n            )\n            return await self.async_step_init()\n\n        current_val = self.current_input_entry.get(FIXED_TARIFF, None)\n\n        return self.async_show_form(\n            step_id=\"fixed_tariff\",\n            data_schema=vol.Schema(\n                {\n                    vol.Optional(FIXED_TARIFF, default=current_val): vol.All(\n                        vol.Coerce(float),\n                        vol.Range(min=0, max=100),\n                    )\n                }\n            ),\n        )\n\n    async def async_step_tariff_sensor(self, user_input=None):\n        if user_input is not None:\n            self.current_input_entry[TARIFF_TYPE] = TARIFF_SENSOR\n            self.current_input_entry[TARIFF_SENSOR] = user_input[TARIFF_SENSOR]\n            self.hass.config_entries.async_update_entry(\n                self._active_config_entry,\n                data=self.updated_entry,\n                options=self._active_config_entry.options,\n            )\n            return await self.async_step_init()\n\n        current_val = self.current_input_entry.get(TARIFF_SENSOR, None)\n\n        return self.async_show_form(\n            step_id=\"tariff_sensor\",\n            data_schema=vol.Schema(\n                {vol.Required(TARIFF_SENSOR): EntitySelector(EntitySelectorConfig())}\n            ),\n        )\n\n    async def async_step_all_done(self, user_input=None):\n        data = {\"time\": time.asctime()}\n        return self.async_create_entry(title=\"\", data=data)\n"
  },
  {
    "path": "custom_components/battery_sim/const.py",
    "content": "\"\"\"Constants for the battery_sim component.\"\"\"\n\nDOMAIN = \"battery_sim\"\n\nBATTERY_TYPE = \"battery\"\n\nBATTERY_PLATFORMS = [\"sensor\", \"switch\", \"button\", \"select\", \"number\"]\n\nMESSAGE_TYPE_GENERAL = \"BatteryResetMessage\"\nMESSAGE_TYPE_BATTERY_RESET_IMP = \"BatteryResetImportSim\"\nMESSAGE_TYPE_BATTERY_RESET_EXP = \"BatteryResetExportSim\"\nMESSAGE_TYPE_BATTERY_UPDATE = \"BatteryUpdateMessage\"\n\nDATA_UTILITY = \"battery_sim_data\"\n\nSETUP_TYPE = \"setup_type\"\nCONFIG_FLOW = \"config_flow\"\nYAML = \"yaml\"\n\nCONF_BATTERY = \"battery\"\nCONF_INPUT_LIST = \"input_list\"\nCONF_IMPORT_SENSOR = \"import_sensor\"\nCONF_SECOND_IMPORT_SENSOR = \"second_import_sensor\"\nCONF_EXPORT_SENSOR = \"export_sensor\"\nCONF_SECOND_EXPORT_SENSOR = \"second_export_sensor\"\nCONF_SOLAR_ENERGY_SENSOR = \"solar_energy_sensor\"\nCONF_NOMINAL_INVERTER_POWER = \"nominal_inverter_power_kw\"\nCONF_BATTERY_SIZE = \"size_kwh\"\nCONF_BATTERY_MAX_DISCHARGE_RATE = \"max_discharge_rate_kw\"\nCONF_BATTERY_MAX_CHARGE_RATE = \"max_charge_rate_kw\"\nCONF_BATTERY_EFFICIENCY = \"efficiency\"  # Legacy key kept for backwards compatibility.\nCONF_BATTERY_DISCHARGE_EFFICIENCY = \"discharge_efficiency\"\nCONF_BATTERY_CHARGE_EFFICIENCY = \"charge_efficiency\"\nATTR_LAST_CHARGE_EFFICIENCY = \"last charge efficiency\"\nATTR_LAST_DISCHARGE_EFFICIENCY = \"last discharge efficiency\"\nMINIMUM_UPDATE_INTERVAL_SECONDS = 5\nCONF_ENERGY_TARIFF = \"energy_tariff\"\nCONF_ENERGY_IMPORT_TARIFF = \"energy_import_tariff\"\nCONF_ENERGY_EXPORT_TARIFF = \"energy_export_tariff\"\nCONF_UNIQUE_NAME = \"unique_name\"\nCONF_RATED_BATTERY_CYCLES = \"rated_battery_cycles\"\nCONF_END_OF_LIFE_DEGRADATION = \"end_of_life_degradation\"\nCONF_UPDATE_FREQUENCY = \"update_frequency\"\nATTR_VALUE = \"value\"\nMETER_TYPE = \"type_of_energy_meter\"\nONE_IMPORT_ONE_EXPORT_METER = \"one_import_one_export\"\nTWO_IMPORT_ONE_EXPORT_METER = \"two_import_one_export\"\nTWO_IMPORT_TWO_EXPORT_METER = \"two_import_two_export\"\nTARIFF_TYPE = \"tariff_type\"\nNO_TARIFF_INFO = \"No tariff information\"\nTARIFF_SENSOR = \"tariff_sensor\"\nFIXED_TARIFF = \"fixed_tariff\"\nTARIFF_SENSOR_ENTITIES = \"Sensors that track tariffs\"\nFIXED_NUMERICAL_TARIFFS = \"Fixed value for tariffs\"\nNEXT_STEP = \"next_step\"\nADD_ANOTHER = \"Add another\"\nALL_DONE = \"All done\"\n\nATTR_SOURCE_ID = \"sources\"\nATTR_STATUS = \"status\"\nPRECISION = 3\nATTR_ENERGY_SAVED = \"total energy saved\"\nATTR_ENERGY_SAVED_TODAY = \"energy_saved_today\"\nATTR_ENERGY_SAVED_WEEK = \"energy_saved_this_week\"\nATTR_ENERGY_SAVED_MONTH = \"energy_saved_this_month\"\nATTR_DATE_RECORDING_STARTED = \"date_recording_started\"\nATTR_ENERGY_BATTERY_OUT = \"battery_energy_out\"\nATTR_ENERGY_BATTERY_IN = \"battery_energy_in\"\nATTR_MONEY_SAVED = \"total_money_saved\"\nATTR_MONEY_SAVED_IMPORT = \"money_saved_on_imports\"\nATTR_MONEY_SAVED_EXPORT = \"extra_money_earned_on_exports\"\nCHARGING_RATE = \"current charging rate\"\nDISCHARGING_RATE = \"current discharging rate\"\nSOLAR_POWER_CAP = \"solar power cap\"\nATTR_CHARGE_PERCENTAGE = \"percentage\"\nGRID_EXPORT_SIM = \"simulated grid export after battery charging\"\nGRID_IMPORT_SIM = \"simulated grid import after battery discharging\"\nGRID_SECOND_EXPORT_SIM = \"simulated second grid export after battery charging\"\nGRID_SECOND_IMPORT_SIM = \"simulated second grid import after battery discharging\"\nICON_CHARGING = \"mdi:battery-charging-50\"\nICON_DISCHARGING = \"mdi:battery-50\"\nICON_FULL = \"mdi:battery\"\nICON_EMPTY = \"mdi:battery-outline\"\nCHARGE_LIMIT = \"charge_limit\"\nDISCHARGE_LIMIT = \"discharge_limit\"\nMINIMUM_SOC = \"minimum_soc\"\nMAXIMUM_SOC = \"maximum_soc\"\nOVERRIDE_CHARGING = \"force_charge\"\nFORCE_DISCHARGE = \"force_discharge\"\nCHARGE_ONLY = \"charge_only\"\nDISCHARGE_ONLY = \"discharge_only\"\nPAUSE_BATTERY = \"pause_battery\"\nRESET_BATTERY = \"reset_battery\"\nDEFAULT_MODE = \"default_mode\"\nPERCENTAGE_ENERGY_IMPORT_SAVED = \"percentage_import_energy_saved\"\nBATTERY_CYCLES = \"battery_cycles\"\nBATTERY_DEGRADATION = \"battery_degradation\"\nSENSOR_ID = \"sensor_id\"\nSENSOR_TYPE = \"sensor_type\"\nTARIFF = \"tariff_sensor_or_value\"\nCONF_SECOND_ENERGY_IMPORT_TARIFF = \"second_energy_import_tariff\"\nCONF_SECOND_ENERGY_EXPORT_TARIFF = \"second_energy_export_tariff\"\nIMPORT = \"Import\"\nEXPORT = \"Export\"\nSOLAR = \"Solar\"\nSIMULATED_SENSOR = \"simulated_sensor\"\n\nBATTERY_MODE = \"Battery_mode_now\"\nMODE_IDLE = \"Idle/Paused\"\nMODE_CHARGING = \"Charging\"\nMODE_DISCHARGING = \"Discharging\"\nMODE_FORCE_CHARGING = \"Forced charging\"\nMODE_FORCE_DISCHARGING = \"Forced discharging\"\nMODE_FULL = \"Full\"\nMODE_EMPTY = \"Empty\"\n\nBATTERY_OPTIONS = {\n    \"BYD Battery Box HVS 5.1kWh\": {\n        CONF_BATTERY_SIZE: 5.1,\n        CONF_BATTERY_CHARGE_EFFICIENCY: 0.98,\n        CONF_BATTERY_DISCHARGE_EFFICIENCY: 0.98,\n        CONF_BATTERY_MAX_CHARGE_RATE: 5.7,\n        CONF_BATTERY_MAX_DISCHARGE_RATE: 5.7,\n    },\n    \"BYD Battery Box HVS 7.7kWh\": {\n        CONF_BATTERY_SIZE: 7.68,\n        CONF_BATTERY_CHARGE_EFFICIENCY: 0.98,\n        CONF_BATTERY_DISCHARGE_EFFICIENCY: 0.98,\n        CONF_BATTERY_MAX_CHARGE_RATE: 5.7,\n        CONF_BATTERY_MAX_DISCHARGE_RATE: 5.7,\n    },\n    \"BYD Battery Box HVS 10.2kWh\": {\n        CONF_BATTERY_SIZE: 10.2,\n        CONF_BATTERY_CHARGE_EFFICIENCY: 0.98,\n        CONF_BATTERY_DISCHARGE_EFFICIENCY: 0.98,\n        CONF_BATTERY_MAX_CHARGE_RATE: 5.7,\n        CONF_BATTERY_MAX_DISCHARGE_RATE: 5.7,\n    },\n    \"BYD Battery Box HVS 12.8kWh\": {\n        CONF_BATTERY_SIZE: 12.8,\n        CONF_BATTERY_CHARGE_EFFICIENCY: 0.98,\n        CONF_BATTERY_DISCHARGE_EFFICIENCY: 0.98,\n        CONF_BATTERY_MAX_CHARGE_RATE: 5.7,\n        CONF_BATTERY_MAX_DISCHARGE_RATE: 5.7,\n    },\n    \"BYD Battery Box HVM 8.3kWh\": {\n        CONF_BATTERY_SIZE: 8.28,\n        CONF_BATTERY_CHARGE_EFFICIENCY: 0.98,\n        CONF_BATTERY_DISCHARGE_EFFICIENCY: 0.98,\n        CONF_BATTERY_MAX_CHARGE_RATE: 7,\n        CONF_BATTERY_MAX_DISCHARGE_RATE: 7,\n    },\n    \"BYD Battery Box HVM 11.0kWh\": {\n        CONF_BATTERY_SIZE: 11.04,\n        CONF_BATTERY_CHARGE_EFFICIENCY: 0.98,\n        CONF_BATTERY_DISCHARGE_EFFICIENCY: 0.98,\n        CONF_BATTERY_MAX_CHARGE_RATE: 10.2,\n        CONF_BATTERY_MAX_DISCHARGE_RATE: 10.2,\n    },\n    \"BYD Battery Box HVM 13.8kWh\": {\n        CONF_BATTERY_SIZE: 13.8,\n        CONF_BATTERY_CHARGE_EFFICIENCY: 0.98,\n        CONF_BATTERY_DISCHARGE_EFFICIENCY: 0.98,\n        CONF_BATTERY_MAX_CHARGE_RATE: 11.5,\n        CONF_BATTERY_MAX_DISCHARGE_RATE: 11.5,\n    },\n    \"BYD Battery Box HVM 16.6kWh\": {\n        CONF_BATTERY_SIZE: 16.56,\n        CONF_BATTERY_CHARGE_EFFICIENCY: 0.98,\n        CONF_BATTERY_DISCHARGE_EFFICIENCY: 0.98,\n        CONF_BATTERY_MAX_CHARGE_RATE: 11.5,\n        CONF_BATTERY_MAX_DISCHARGE_RATE: 11.5,\n    },\n    \"BYD Battery Box HVM 19.3kWh\": {\n        CONF_BATTERY_SIZE: 19.32,\n        CONF_BATTERY_CHARGE_EFFICIENCY: 0.98,\n        CONF_BATTERY_DISCHARGE_EFFICIENCY: 0.98,\n        CONF_BATTERY_MAX_CHARGE_RATE: 11.5,\n        CONF_BATTERY_MAX_DISCHARGE_RATE: 11.5,\n    },\n    \"BYD Battery Box HVM 22.1kWh\": {\n        CONF_BATTERY_SIZE: 22.08,\n        CONF_BATTERY_CHARGE_EFFICIENCY: 0.98,\n        CONF_BATTERY_DISCHARGE_EFFICIENCY: 0.98,\n        CONF_BATTERY_MAX_CHARGE_RATE: 11.5,\n        CONF_BATTERY_MAX_DISCHARGE_RATE: 11.5,\n    },\n    \"Enphase 3T (2nd Gen)\": {\n        CONF_BATTERY_SIZE: 3.36,\n        CONF_BATTERY_CHARGE_EFFICIENCY: 0.94,\n        CONF_BATTERY_DISCHARGE_EFFICIENCY: 0.94,\n        CONF_BATTERY_MAX_DISCHARGE_RATE: 1.92,\n        CONF_BATTERY_MAX_CHARGE_RATE: 1.28,\n    },\n    \"Enphase 10T (2nd Gen)\": {\n        CONF_BATTERY_SIZE: 10.08,\n        CONF_BATTERY_CHARGE_EFFICIENCY: 0.94,\n        CONF_BATTERY_DISCHARGE_EFFICIENCY: 0.94,\n        CONF_BATTERY_MAX_DISCHARGE_RATE: 5.0,\n        CONF_BATTERY_MAX_CHARGE_RATE: 3.84,\n    },\n    \"Enphase 5P (3rd Gen)\": {\n        CONF_BATTERY_SIZE: 5.0,\n        CONF_BATTERY_CHARGE_EFFICIENCY: 0.94,\n        CONF_BATTERY_DISCHARGE_EFFICIENCY: 0.94,\n        CONF_BATTERY_MAX_DISCHARGE_RATE: 5.7,\n        CONF_BATTERY_MAX_CHARGE_RATE: 3.84,\n    },\n    \"Fronius Reserva 6.3\": {\n        CONF_BATTERY_SIZE: 6.31,\n        CONF_BATTERY_CHARGE_EFFICIENCY: 0.975,\n        CONF_BATTERY_DISCHARGE_EFFICIENCY: 0.975,\n        CONF_BATTERY_MAX_CHARGE_RATE: 5.94,\n        CONF_BATTERY_MAX_DISCHARGE_RATE: 5.94,\n    },\n    \"Fronius Reserva 9.5\": {\n        CONF_BATTERY_SIZE: 9.47,\n        CONF_BATTERY_CHARGE_EFFICIENCY: 0.975,\n        CONF_BATTERY_DISCHARGE_EFFICIENCY: 0.975,\n        CONF_BATTERY_MAX_CHARGE_RATE: 8.91,\n        CONF_BATTERY_MAX_DISCHARGE_RATE: 8.91,\n    },\n    \"Fronius Reserva 12.6\": {\n        CONF_BATTERY_SIZE: 12.63,\n        CONF_BATTERY_CHARGE_EFFICIENCY: 0.975,\n        CONF_BATTERY_DISCHARGE_EFFICIENCY: 0.975,\n        CONF_BATTERY_MAX_CHARGE_RATE: 11.88,\n        CONF_BATTERY_MAX_DISCHARGE_RATE: 11.88,\n    },\n    \"Fronius Reserva 15.8\": {\n        CONF_BATTERY_SIZE: 15.79,\n        CONF_BATTERY_CHARGE_EFFICIENCY: 0.975,\n        CONF_BATTERY_DISCHARGE_EFFICIENCY: 0.975,\n        CONF_BATTERY_MAX_CHARGE_RATE: 14.85,\n        CONF_BATTERY_MAX_DISCHARGE_RATE: 14.85,\n    },\n    \"HomeWizard Energy Plug-in Battery 2.7kWh\": {\n        CONF_BATTERY_SIZE: 2.473,\n        CONF_BATTERY_CHARGE_EFFICIENCY: 0.865,\n        CONF_BATTERY_DISCHARGE_EFFICIENCY: 0.865,\n        CONF_BATTERY_MAX_CHARGE_RATE: 0.8,\n        CONF_BATTERY_MAX_DISCHARGE_RATE: 0.8,\n    },\n    \"Huawei Luna2000 5kW\": {\n        CONF_BATTERY_SIZE: 5.0,\n        CONF_BATTERY_CHARGE_EFFICIENCY: 0.95,\n        CONF_BATTERY_DISCHARGE_EFFICIENCY: 0.95,\n        CONF_BATTERY_MAX_CHARGE_RATE: 2.5,\n        CONF_BATTERY_MAX_DISCHARGE_RATE: 2.5,\n    },\n    \"Huawei Luna2000 10kW\": {\n        CONF_BATTERY_SIZE: 10.0,\n        CONF_BATTERY_CHARGE_EFFICIENCY: 0.95,\n        CONF_BATTERY_DISCHARGE_EFFICIENCY: 0.95,\n        CONF_BATTERY_MAX_CHARGE_RATE: 5,\n        CONF_BATTERY_MAX_DISCHARGE_RATE: 5,\n    },\n    \"Huawei Luna2000 15kW\": {\n        CONF_BATTERY_SIZE: 15.0,\n        CONF_BATTERY_CHARGE_EFFICIENCY: 0.95,\n        CONF_BATTERY_DISCHARGE_EFFICIENCY: 0.95,\n        CONF_BATTERY_MAX_CHARGE_RATE: 5,\n        CONF_BATTERY_MAX_DISCHARGE_RATE: 5,\n    },\n    \"LG Chem\": {\n        CONF_BATTERY_SIZE: 9.3,\n        CONF_BATTERY_CHARGE_EFFICIENCY: 0.975,\n        CONF_BATTERY_DISCHARGE_EFFICIENCY: 0.975,\n        CONF_BATTERY_MAX_DISCHARGE_RATE: 5.0,\n        CONF_BATTERY_MAX_CHARGE_RATE: 3.3,\n    },\n    \"Marstek Venus E 5.12kWh (2nd Gen)\": {\n        CONF_BATTERY_SIZE: 5.12,\n        CONF_BATTERY_CHARGE_EFFICIENCY: 0.90,\n        CONF_BATTERY_DISCHARGE_EFFICIENCY: 0.90,\n        CONF_BATTERY_MAX_CHARGE_RATE: 2.5,\n        CONF_BATTERY_MAX_DISCHARGE_RATE: 0.8,\n    },\n    \"Marstek Venus E 5.12kWh (3nd Gen)\": {\n        CONF_BATTERY_SIZE: 5.12,\n        CONF_BATTERY_CHARGE_EFFICIENCY: 0.91,\n        CONF_BATTERY_DISCHARGE_EFFICIENCY: 0.91,\n        CONF_BATTERY_MAX_CHARGE_RATE: 2.5,\n        CONF_BATTERY_MAX_DISCHARGE_RATE: 0.8,\n    },\n    \"Pika Harbour\": {\n        CONF_BATTERY_SIZE: 8.6,\n        CONF_BATTERY_CHARGE_EFFICIENCY: 0.975,\n        CONF_BATTERY_DISCHARGE_EFFICIENCY: 0.975,\n        CONF_BATTERY_MAX_DISCHARGE_RATE: 4.2,\n        CONF_BATTERY_MAX_CHARGE_RATE: 4.2,\n    },\n    \"Sonnen Eco\": {\n        CONF_BATTERY_SIZE: 5.0,\n        CONF_BATTERY_CHARGE_EFFICIENCY: 0.92,\n        CONF_BATTERY_DISCHARGE_EFFICIENCY: 0.92,\n        CONF_BATTERY_MAX_DISCHARGE_RATE: 2.5,\n        CONF_BATTERY_MAX_CHARGE_RATE: 2.5,\n    },\n    \"Sessy\": {\n        CONF_BATTERY_SIZE: 5.0,\n        CONF_BATTERY_CHARGE_EFFICIENCY: 0.92,\n        CONF_BATTERY_DISCHARGE_EFFICIENCY: 0.92,\n        CONF_BATTERY_MAX_CHARGE_RATE: 2.2,\n        CONF_BATTERY_MAX_DISCHARGE_RATE: 1.7,\n    },\n    \"Solax 5.8kWh Master\": {\n        CONF_BATTERY_SIZE: 5.1,\n        CONF_BATTERY_CHARGE_EFFICIENCY: 0.975,\n        CONF_BATTERY_DISCHARGE_EFFICIENCY: 0.975,\n        CONF_BATTERY_MAX_CHARGE_RATE: 4,\n        CONF_BATTERY_MAX_DISCHARGE_RATE: 4,\n    },\n    \"SolaX X3-IES-P 5kW\": {\n        CONF_BATTERY_SIZE: 5.1,\n        CONF_BATTERY_CHARGE_EFFICIENCY: 0.975,\n        CONF_BATTERY_DISCHARGE_EFFICIENCY: 0.975,\n        CONF_BATTERY_MAX_CHARGE_RATE: 5.1,\n        CONF_BATTERY_MAX_DISCHARGE_RATE: 5.1,\n    },\n    \"SolaX X3-IES-P 10kW\": {\n        CONF_BATTERY_SIZE: 10.2,\n        CONF_BATTERY_CHARGE_EFFICIENCY: 0.975,\n        CONF_BATTERY_DISCHARGE_EFFICIENCY: 0.975,\n        CONF_BATTERY_MAX_CHARGE_RATE: 10.2,\n        CONF_BATTERY_MAX_DISCHARGE_RATE: 10.2,\n    },\n    \"SolaX X3-IES-P 15kW\": {\n        CONF_BATTERY_SIZE: 15.3,\n        CONF_BATTERY_CHARGE_EFFICIENCY: 0.975,\n        CONF_BATTERY_DISCHARGE_EFFICIENCY: 0.975,\n        CONF_BATTERY_MAX_CHARGE_RATE: 15.0,\n        CONF_BATTERY_MAX_DISCHARGE_RATE: 15.0,\n    },\n    \"SolaX X3-IES-P 20kW\": {\n        CONF_BATTERY_SIZE: 20.4,\n        CONF_BATTERY_CHARGE_EFFICIENCY: 0.975,\n        CONF_BATTERY_DISCHARGE_EFFICIENCY: 0.975,\n        CONF_BATTERY_MAX_CHARGE_RATE: 15.0,\n        CONF_BATTERY_MAX_DISCHARGE_RATE: 15.0,\n    },\n    \"SolaX X3-IES-P 25kW\": {\n        CONF_BATTERY_SIZE: 25.6,\n        CONF_BATTERY_CHARGE_EFFICIENCY: 0.975,\n        CONF_BATTERY_DISCHARGE_EFFICIENCY: 0.975,\n        CONF_BATTERY_MAX_CHARGE_RATE: 15.0,\n        CONF_BATTERY_MAX_DISCHARGE_RATE: 15.0,\n    },\n    \"SolaX X3-IES-P 30kW\": {\n        CONF_BATTERY_SIZE: 30.7,\n        CONF_BATTERY_CHARGE_EFFICIENCY: 0.975,\n        CONF_BATTERY_DISCHARGE_EFFICIENCY: 0.975,\n        CONF_BATTERY_MAX_CHARGE_RATE: 15.0,\n        CONF_BATTERY_MAX_DISCHARGE_RATE: 15.0,\n    },\n    \"Tesla Powerwall\": {\n        CONF_BATTERY_SIZE: 13.5,\n        CONF_BATTERY_CHARGE_EFFICIENCY: 0.95,\n        CONF_BATTERY_DISCHARGE_EFFICIENCY: 0.95,\n        CONF_BATTERY_MAX_DISCHARGE_RATE: 5.0,\n        CONF_BATTERY_MAX_CHARGE_RATE: 3.68,\n    },\n    \"Custom\": {},\n}\n"
  },
  {
    "path": "custom_components/battery_sim/helpers.py",
    "content": "import re\n\nfrom .const import (\n    CONF_ENERGY_EXPORT_TARIFF,\n    CONF_ENERGY_IMPORT_TARIFF,\n    CONF_ENERGY_TARIFF,\n    CONF_EXPORT_SENSOR,\n    CONF_IMPORT_SENSOR,\n    CONF_SECOND_EXPORT_SENSOR,\n    CONF_SECOND_IMPORT_SENSOR,\n    FIXED_NUMERICAL_TARIFFS,\n    GRID_EXPORT_SIM,\n    GRID_IMPORT_SIM,\n    GRID_SECOND_EXPORT_SIM,\n    GRID_SECOND_IMPORT_SIM,\n    NO_TARIFF_INFO,\n    FIXED_TARIFF,\n    TARIFF_TYPE,\n    SENSOR_ID,\n    SENSOR_TYPE,\n    TARIFF_SENSOR,\n    CONF_SECOND_ENERGY_IMPORT_TARIFF,\n    CONF_SECOND_ENERGY_EXPORT_TARIFF,\n    IMPORT,\n    EXPORT,\n    SIMULATED_SENSOR,\n)\n\n\"\"\"For backwards compatability with old configs\"\"\"\n\n\ndef generate_input_list(config):\n    tariff_type: str = TARIFF_SENSOR\n    if TARIFF_TYPE in config:\n        if config[TARIFF_TYPE] == NO_TARIFF_INFO:\n            tariff_type = NO_TARIFF_INFO\n        elif config[TARIFF_TYPE] == FIXED_NUMERICAL_TARIFFS:\n            tariff_type = FIXED_TARIFF\n\n    inputs = [\n        {\n            SENSOR_ID: config[CONF_IMPORT_SENSOR],\n            SENSOR_TYPE: IMPORT,\n            SIMULATED_SENSOR: GRID_IMPORT_SIM,\n            TARIFF_TYPE: tariff_type,\n        },\n        {\n            SENSOR_ID: config[CONF_EXPORT_SENSOR],\n            SENSOR_TYPE: EXPORT,\n            SIMULATED_SENSOR: GRID_EXPORT_SIM,\n            TARIFF_TYPE: tariff_type,\n        },\n    ]\n    if len(config.get(CONF_SECOND_IMPORT_SENSOR, \"\")) > 6:\n        inputs.append(\n            {\n                SENSOR_ID: config[CONF_SECOND_IMPORT_SENSOR],\n                SENSOR_TYPE: IMPORT,\n                SIMULATED_SENSOR: GRID_SECOND_IMPORT_SIM,\n                TARIFF_TYPE: tariff_type,\n            }\n        )\n    if len(config.get(CONF_SECOND_EXPORT_SENSOR, \"\")) > 6:\n        inputs.append(\n            {\n                SENSOR_ID: config[CONF_SECOND_EXPORT_SENSOR],\n                SENSOR_TYPE: EXPORT,\n                SIMULATED_SENSOR: GRID_SECOND_EXPORT_SIM,\n                TARIFF_TYPE: tariff_type,\n            }\n        )\n\n    \"\"\"Default sensor entities for backwards compatibility\"\"\"\n    if CONF_ENERGY_IMPORT_TARIFF in config:\n        inputs[0][tariff_type] = config[CONF_ENERGY_IMPORT_TARIFF]\n    elif CONF_ENERGY_TARIFF in config:\n        \"\"\"For backwards compatibility\"\"\"\n        inputs[0][tariff_type] = config[CONF_ENERGY_TARIFF]\n\n    if CONF_ENERGY_EXPORT_TARIFF in config:\n        inputs[1][tariff_type] = config[CONF_ENERGY_EXPORT_TARIFF]\n\n    def _set_tariff_for_sensor(simulated_sensor, tariff_config_key):\n        if tariff_config_key not in config:\n            return\n        matching_input = next(\n            (\n                input_entry\n                for input_entry in inputs\n                if input_entry[SIMULATED_SENSOR] == simulated_sensor\n            ),\n            None,\n        )\n        if matching_input is not None:\n            matching_input[tariff_type] = config[tariff_config_key]\n\n    _set_tariff_for_sensor(\n        GRID_SECOND_IMPORT_SIM,\n        CONF_SECOND_ENERGY_IMPORT_TARIFF,\n    )\n    _set_tariff_for_sensor(\n        GRID_SECOND_EXPORT_SIM,\n        CONF_SECOND_ENERGY_EXPORT_TARIFF,\n    )\n    return inputs\n\n\ndef parse_efficiency_curve(raw_value):\n    \"\"\"Parse an efficiency config value into sorted (power_kw, efficiency) points.\"\"\"\n    if isinstance(raw_value, (int, float)):\n        value = float(raw_value)\n        _validate_efficiency(value)\n        return [(0.0, value)]\n\n    if raw_value is None:\n        raise ValueError(\"Efficiency value is required\")\n\n    text = str(raw_value).strip()\n    if not text:\n        raise ValueError(\"Efficiency value is required\")\n\n    try:\n        value = float(text)\n    except ValueError:\n        value = None\n\n    if value is not None:\n        _validate_efficiency(value)\n        return [(0.0, value)]\n\n    normalized = text.replace(\";\", \",\")\n    pair_matches = re.findall(\n        r\"\\(?\\s*(-?\\d+(?:\\.\\d+)?)\\s*[,:\\s]\\s*(-?\\d+(?:\\.\\d+)?)\\s*\\)?\",\n        normalized,\n    )\n    if not pair_matches:\n        raise ValueError(\n            \"Use a number like 0.95 or power/efficiency pairs like 0:0.9, 5:0.95\"\n        )\n\n    points = []\n    for power_text, efficiency_text in pair_matches:\n        power = float(power_text)\n        efficiency = float(efficiency_text)\n        if power < 0:\n            raise ValueError(\"Efficiency curve power values must be >= 0\")\n        _validate_efficiency(efficiency)\n        points.append((power, efficiency))\n\n    points.sort(key=lambda item: item[0])\n    deduplicated_points = []\n    for power, efficiency in points:\n        if deduplicated_points and power == deduplicated_points[-1][0]:\n            deduplicated_points[-1] = (power, efficiency)\n        else:\n            deduplicated_points.append((power, efficiency))\n    return deduplicated_points\n\n\ndef validate_efficiency_config(raw_value):\n    \"\"\"Validate the configured efficiency value or curve and return the raw value.\"\"\"\n    parse_efficiency_curve(raw_value)\n    return raw_value\n\n\ndef interpolate_efficiency(curve_points, power_kw):\n    \"\"\"Return the efficiency for the requested power using linear interpolation.\"\"\"\n    if not curve_points:\n        raise ValueError(\"Efficiency curve must contain at least one point\")\n\n    if len(curve_points) == 1 or power_kw <= curve_points[0][0]:\n        return curve_points[0][1]\n\n    for (start_power, start_efficiency), (end_power, end_efficiency) in zip(\n        curve_points, curve_points[1:]\n    ):\n        if power_kw <= end_power:\n            if end_power == start_power:\n                return end_efficiency\n            ratio = (power_kw - start_power) / (end_power - start_power)\n            return start_efficiency + ratio * (end_efficiency - start_efficiency)\n\n    return curve_points[-1][1]\n\n\ndef _validate_efficiency(value):\n    if not 0 <= value <= 1:\n        raise ValueError(\"Efficiency values must be between 0 and 1\")\n"
  },
  {
    "path": "custom_components/battery_sim/manifest.json",
    "content": "{\n  \"domain\": \"battery_sim\",\n  \"name\": \"Home Battery Simulation\",\n  \"codeowners\": [\"@hif2k1\", \"@dewi-ny-je\"],\n  \"config_flow\": true,\n  \"documentation\": \"https://github.com/hif2k1/battery_sim/\",\n  \"iot_class\": \"local_push\",\n  \"issue_tracker\": \"https://github.com/hif2k1/battery_sim/issues\",\n  \"quality_scale\": \"internal\",\n  \"requirements\": [],\n  \"version\": \"2.3.0\"\n}\n"
  },
  {
    "path": "custom_components/battery_sim/number.py",
    "content": "#from homeassistant.components.number import NumberEntity\nfrom homeassistant.components.number import RestoreNumber\nfrom homeassistant.helpers.entity_platform import AddEntitiesCallback\nfrom homeassistant.core import HomeAssistant\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.helpers.typing import DiscoveryInfoType\n\nfrom homeassistant.components.number import NumberEntity\n\nfrom .const import (\n    DOMAIN,\n    CHARGE_LIMIT,\n    DISCHARGE_LIMIT,\n    MINIMUM_SOC,\n    MAXIMUM_SOC,\n)\n \nimport logging\n\n_LOGGER = logging.getLogger(__name__)\n\nBATTERY_SLIDERS = [\n    {\n        \"name\": CHARGE_LIMIT,\n        \"key\": \"charge_limit\",\n        \"icon\": \"mdi:car-speed-limiter\",\n        \"unit\": \"kW\",\n        \"precision\": 0.01,\n    },\n    {\n        \"name\": DISCHARGE_LIMIT,\n        \"key\": \"discharge_limit\",\n        \"icon\": \"mdi:car-speed-limiter\",\n        \"unit\": \"kW\",\n        \"precision\": 0.01,\n    },\n    {\n        \"name\": MINIMUM_SOC,\n        \"key\": \"minimum_soc\",\n        \"icon\": \"mdi:battery-10\",\n        \"unit\": \"%\",\n        \"precision\": 1,\n    },\n    {\n        \"name\": MAXIMUM_SOC,\n        \"key\": \"maximum_soc\",\n        \"icon\": \"mdi:battery-90\",\n        \"unit\": \"%\",\n        \"precision\": 1,\n    },\n]\n \nasync def async_setup_entry(hass, config_entry, async_add_entities):\n    handle = hass.data[DOMAIN][config_entry.entry_id]\n\n    sliders = [\n        BatterySlider(handle, slider[\"name\"], slider[\"key\"], slider[\"icon\"], slider[\"unit\"], slider[\"precision\"])\n        for slider in BATTERY_SLIDERS\n    ]\n\n    async_add_entities(sliders)\n\n    return True\n\nasync def async_setup_platform( hass, configuration, async_add_entities, discovery_info=None ):\n    if discovery_info is None:\n        _LOGGER.error(\"This platform is only available through discovery\")\n        return\n\n    for conf in discovery_info:\n        battery = conf[CONF_BATTERY]\n        handle = hass.data[DOMAIN][battery]\n\n    sliders = [\n        BatterySlider(handle, slider[\"name\"], slider[\"key\"], slider[\"icon\"], slider[\"unit\"], slider[\"precision\"])\n        for slider in BATTERY_SLIDERS\n    ]\n\n    async_add_entities(sliders)\n\n    return True\n \n   \nclass BatterySlider(RestoreNumber):\n    \"\"\"Slider to set a numeric parameter for the simulated battery.\"\"\"\n\n    def __init__(self, handle, slider_type, key, icon, unit, precision):\n        \"\"\"Initialize the slider.\"\"\"\n        self.handle = handle\n        self._key = key\n        self._icon = icon\n        self._slider_type = slider_type\n        self._precision = precision\n        self._device_name = handle._name\n        self._device_identifier = handle.device_identifier\n        self._name = f\"{handle._name} \".replace(\"_\", \" \") + f\"{slider_type}\".replace(\"_\", \" \").capitalize()\n        self._attr_unique_id = f\"{handle._name} - {slider_type}\"\n        if key == \"charge_limit\":\n            self._max_value = handle._max_charge_rate\n            self._value = self._max_value\n        elif key == \"discharge_limit\":               \n            self._max_value = handle._max_discharge_rate\n            self._value = self._max_value\n        elif key == \"minimum_soc\":               \n            self._max_value = 100          \n            self._value = 0\n        elif key == \"maximum_soc\":               \n            self._max_value = 100\n            self._value = self._max_value\n        else:\n            _LOGGER.debug(\"Reached undefined state in number.py\")\n        self._attr_icon = icon\n        self._attr_unit_of_measurement = unit\n        self._attr_mode = \"box\"\n        \n    @property\n    def unique_id(self):\n        \"\"\"Return uniqueid.\"\"\"\n        return self._attr_unique_id\n\n    @property\n    def name(self):\n        return self._name\n\n    @property\n    def device_info(self):\n        return {\n            \"name\": self._device_name,\n            \"identifiers\": {self._device_identifier},\n        }\n        \n    @property\n    def native_min_value(self):\n        return 0.00\n\n    @property\n    def native_max_value(self):\n        return self._max_value\n\n    @property\n    def native_step(self):\n        return self._precision\n\n    @property\n    def native_value(self):\n        return self._value\n\n    async def async_set_native_value(self, value: float) -> None:\n        self._value = value\n     \n        self.handle.set_slider_limit(value, self._key)\n        # Recompute immediately so UI/control changes take effect right away.\n        self.handle.async_trigger_update()\n        self.async_write_ha_state()\n\n    async def async_added_to_hass(self):\n        \"\"\"Restore previously saved value.\"\"\"\n        await super().async_added_to_hass()\n\n        if (last_number_data := await self.async_get_last_number_data()) is not None:\n            self._value = last_number_data.native_value\n            _LOGGER.debug(\"Restored %s to %.2f\", self._key, self._value)\n            self.handle.set_slider_limit(self._value, self._key)  # Restore to handle too\n"
  },
  {
    "path": "custom_components/battery_sim/select.py",
    "content": "\"\"\"Select platform for Battery Sim.\"\"\"\nimport logging\n\nfrom homeassistant.components.select import SelectEntity\n\nfrom .const import (\n    DOMAIN,\n    CONF_BATTERY,\n    OVERRIDE_CHARGING,\n    PAUSE_BATTERY,\n    FORCE_DISCHARGE,\n    CHARGE_ONLY,\n    DISCHARGE_ONLY,\n    DEFAULT_MODE,\n    ICON_FULL,\n)\n\n_LOGGER = logging.getLogger(__name__)\n\n\nasync def async_setup_entry(hass, config_entry, async_add_entities):\n    handle = hass.data[DOMAIN][config_entry.entry_id]\n    async_add_entities([BatteryMode(handle)])\n    return True\n\n\nasync def async_setup_platform(\n    hass, configuration, async_add_entities, discovery_info=None\n):\n    if discovery_info is None:\n        _LOGGER.error(\"This platform is only available through discovery\")\n        return\n\n    for conf in discovery_info:\n        battery = conf[CONF_BATTERY]\n        handle = hass.data[DOMAIN][battery]\n\n    async_add_entities([BatteryMode(handle)])\n    return True\n\n\nclass BatteryMode(SelectEntity):\n    \"\"\"Select to set the battery operating mode.\"\"\"\n\n    def __init__(self, handle):\n        self.handle = handle\n        self._device_name = handle._name\n        self._device_identifier = handle.device_identifier\n        self._name = f\"{handle._name} \".replace(\"_\", \" \") + \"Battery Mode\"\n        self._attr_unique_id = f\"{handle._name} - Battery Mode\"\n        self._internal_options = [\n            DEFAULT_MODE,\n            OVERRIDE_CHARGING,\n            PAUSE_BATTERY,\n            FORCE_DISCHARGE,\n            CHARGE_ONLY,\n            DISCHARGE_ONLY,\n        ]\n\n    @property\n    def unique_id(self):\n        return self._attr_unique_id\n\n    @property\n    def name(self):\n        return self._name\n\n    @property\n    def device_info(self):\n        return {\n            \"name\": self._device_name,\n            \"identifiers\": {self._device_identifier},\n        }\n\n    @property\n    def icon(self):\n        return ICON_FULL\n\n    @property\n    def current_option(self):\n        return self.handle._battery_mode.replace(\"_\", \" \").capitalize()\n\n    @property\n    def options(self):\n        return [opt.replace(\"_\", \" \").capitalize() for opt in self._internal_options]\n\n    async def async_select_option(self, option: str):\n        internal_option = next(\n            (\n                opt\n                for opt in self._internal_options\n                if opt.replace(\"_\", \" \").capitalize() == option\n            ),\n            None,\n        )\n\n        if internal_option is None:\n            _LOGGER.warning(\"Invalid option selected: %s\", option)\n            return\n\n        self.handle._battery_mode = internal_option\n        self.handle.async_trigger_update()\n        self.schedule_update_ha_state(True)\n"
  },
  {
    "path": "custom_components/battery_sim/sensor.py",
    "content": "\"\"\"Simulated battery and associated sensors.\"\"\"\nimport time\nimport logging\n\nimport homeassistant.util.dt as dt_util\nfrom homeassistant.helpers.dispatcher import dispatcher_send, async_dispatcher_connect\n\nfrom homeassistant.components.sensor import (\n    SensorDeviceClass,\n    SensorEntity,\n    SensorStateClass,\n    ATTR_LAST_RESET,\n)\nfrom homeassistant.helpers.restore_state import RestoreEntity\nfrom homeassistant.const import (\n    STATE_UNAVAILABLE,\n    STATE_UNKNOWN,\n    UnitOfPower,\n    UnitOfEnergy,\n)\n\nfrom .const import (\n    DOMAIN,\n    CONF_BATTERY,\n    CONF_BATTERY_CHARGE_EFFICIENCY,\n    CONF_BATTERY_DISCHARGE_EFFICIENCY,\n    CONF_BATTERY_EFFICIENCY,\n    CONF_BATTERY_MAX_DISCHARGE_RATE,\n    CONF_BATTERY_MAX_CHARGE_RATE,\n    CONF_BATTERY_SIZE,\n    ATTR_MONEY_SAVED,\n    ATTR_MONEY_SAVED_IMPORT,\n    ATTR_MONEY_SAVED_EXPORT,\n    ATTR_LAST_CHARGE_EFFICIENCY,\n    ATTR_LAST_DISCHARGE_EFFICIENCY,\n    ATTR_SOURCE_ID,\n    ATTR_STATUS,\n    ATTR_ENERGY_SAVED,\n    ATTR_DATE_RECORDING_STARTED,\n    BATTERY_MODE,\n    ATTR_CHARGE_PERCENTAGE,\n    ATTR_ENERGY_BATTERY_OUT,\n    ATTR_ENERGY_BATTERY_IN,\n    CHARGING_RATE,\n    DISCHARGING_RATE,\n    SOLAR_POWER_CAP,\n    SENSOR_TYPE,\n    EXPORT,\n    SIMULATED_SENSOR,\n    ICON_CHARGING,\n    ICON_DISCHARGING,\n    ICON_FULL,\n    ICON_EMPTY,\n    PERCENTAGE_ENERGY_IMPORT_SAVED,\n    MODE_CHARGING,\n    MODE_FORCE_CHARGING,\n    MODE_FULL,\n    MODE_EMPTY,\n    BATTERY_CYCLES,\n    BATTERY_DEGRADATION,\n    CONF_END_OF_LIFE_DEGRADATION,\n    CONF_RATED_BATTERY_CYCLES,\n    MESSAGE_TYPE_BATTERY_UPDATE,\n    SENSOR_ID,\n)\n\n_LOGGER = logging.getLogger(__name__)\n_INVALID_RESTORED_STATES = {None, \"\", STATE_UNKNOWN, STATE_UNAVAILABLE}\n\nDEVICE_CLASS_MAP = {\n    UnitOfEnergy.WATT_HOUR: SensorDeviceClass.ENERGY,\n    UnitOfEnergy.KILO_WATT_HOUR: SensorDeviceClass.ENERGY,\n}\n\n\nasync def async_setup_entry(hass, config_entry, async_add_entities):\n    handle = hass.data[DOMAIN][config_entry.entry_id]\n    sensors = await define_sensors(hass, handle)\n    async_add_entities(sensors)\n\n\nasync def async_setup_platform(\n    hass, configuration, async_add_entities, discovery_info=None\n):\n    if discovery_info is None:\n        return\n\n    for conf in discovery_info:\n        battery = conf[CONF_BATTERY]\n        handle = hass.data[DOMAIN][battery]\n        sensors = await define_sensors(hass, handle)\n        async_add_entities(sensors)\n\n\nasync def define_sensors(hass, handle):\n    sensors = []\n    sensors.append(\n        DisplayOnlySensor(\n            handle,\n            ATTR_ENERGY_SAVED,\n            SensorDeviceClass.ENERGY,\n            UnitOfEnergy.KILO_WATT_HOUR,\n        )\n    )\n    sensors.append(\n        DisplayOnlySensor(\n            handle,\n            ATTR_ENERGY_BATTERY_OUT,\n            SensorDeviceClass.ENERGY,\n            UnitOfEnergy.KILO_WATT_HOUR,\n        )\n    )\n    sensors.append(\n        DisplayOnlySensor(\n            handle,\n            ATTR_ENERGY_BATTERY_IN,\n            SensorDeviceClass.ENERGY,\n            UnitOfEnergy.KILO_WATT_HOUR,\n        )\n    )\n    sensors.append(\n        DisplayOnlySensor(\n            handle, CHARGING_RATE, SensorDeviceClass.POWER, UnitOfPower.KILO_WATT\n        )\n    )\n    sensors.append(\n        DisplayOnlySensor(\n            handle, DISCHARGING_RATE, SensorDeviceClass.POWER, UnitOfPower.KILO_WATT\n        )\n    )\n    if handle._solar_entity_id is not None:\n        sensors.append(\n            DisplayOnlySensor(\n                handle, SOLAR_POWER_CAP, SensorDeviceClass.POWER, UnitOfPower.KILO_WATT\n            )\n        )\n    sensors.append(DisplayOnlySensor(handle, ATTR_LAST_CHARGE_EFFICIENCY, None, None))\n    sensors.append(\n        DisplayOnlySensor(handle, ATTR_LAST_DISCHARGE_EFFICIENCY, None, None)\n    )\n    for input in handle._inputs:\n        sensors.append(\n            DisplayOnlySensor(\n                handle,\n                input[SIMULATED_SENSOR],\n                SensorDeviceClass.ENERGY,\n                UnitOfEnergy.KILO_WATT_HOUR,\n            )\n        )\n\n    sensors.append(DisplayOnlySensor(handle, BATTERY_CYCLES, None, None))\n    sensors.append(DisplayOnlySensor(handle, BATTERY_DEGRADATION, None, None))\n\n    sensors.append(\n        DisplayOnlySensor(\n            handle,\n            ATTR_MONEY_SAVED_IMPORT,\n            SensorDeviceClass.MONETARY,\n            hass.config.currency,\n        )\n    )\n    sensors.append(\n        DisplayOnlySensor(\n            handle,\n            ATTR_MONEY_SAVED,\n            SensorDeviceClass.MONETARY,\n            hass.config.currency,\n        )\n    )\n\n    sensors.append(\n        DisplayOnlySensor(\n            handle,\n            ATTR_MONEY_SAVED_EXPORT,\n            SensorDeviceClass.MONETARY,\n            hass.config.currency,\n        )\n    )\n    sensors.append(SimulatedBattery(handle))\n    sensors.append(BatteryStatus(handle, BATTERY_MODE))\n    return sensors\n\n\nclass DisplayOnlySensor(RestoreEntity, SensorEntity):\n    \"\"\"\n    Representation of a sensor.\n\n    This reprisentation simply displays a value calculated\n    in the __init__ file.\n    \"\"\"\n\n    _attr_should_poll = False\n\n    def __init__(self, handle, sensor_name, type_of_sensor, units):\n        \"\"\"Initialize the display only sensors for the battery.\"\"\"\n        self._handle = handle\n        self._units = units\n        self._name = f\"{handle._name} \".replace(\"_\", \" \") + f\"{sensor_name}\".replace(\"_\", \" \").capitalize()\n        self._attr_unique_id = f\"{handle._name} - {sensor_name}\"\n        self._device_name = handle._name\n        self._device_identifier = handle.device_identifier\n        self._sensor_type = sensor_name\n        self._type_of_sensor = type_of_sensor\n        self._last_reset = dt_util.utcnow()\n        self._available = False\n\n    @property\n    def _supports_last_reset(self):\n        \"\"\"Return True when Home Assistant allows last_reset for this sensor.\"\"\"\n        return self.state_class == SensorStateClass.TOTAL\n\n    async def async_added_to_hass(self):\n        \"\"\"Subscribe for update from the battery.\"\"\"\n        await super().async_added_to_hass()\n\n        state = await self.async_get_last_state()\n\n        if state:\n            if state.state in _INVALID_RESTORED_STATES:\n                _LOGGER.debug(\n                    \"Ignoring invalid restored state '%s' for sensor '%s'.\",\n                    state.state,\n                    self._sensor_type,\n                )\n            else:\n                try:\n                    self._handle._sensors[self._sensor_type] = float(state.state)\n                    last_reset = state.attributes.get(ATTR_LAST_RESET)\n                    if self._supports_last_reset and last_reset is not None:\n                        parsed_last_reset = dt_util.parse_datetime(last_reset)\n                        if parsed_last_reset is not None:\n                            self._last_reset = dt_util.as_utc(parsed_last_reset)\n                    self._available = True\n                    await self.async_update_ha_state(True)\n                except (TypeError, ValueError):\n                    _LOGGER.debug(\n                        \"Sensor state '%s' not restored properly for '%s'.\",\n                        state.state,\n                        self._sensor_type,\n                    )\n                    self._available = False\n        else:\n            _LOGGER.debug(\"No sensor state - presume new battery.\")\n            self._available = False\n\n        async def async_update_state():\n            \"\"\"Update sensor state.\"\"\"\n            if self._handle._sensors[self._sensor_type] is not None:\n                self._available = True\n            await self.async_update_ha_state(True)\n\n        async_dispatcher_connect(\n            self.hass,\n            f\"{self._device_name}-{MESSAGE_TYPE_BATTERY_UPDATE}\",\n            async_update_state,\n        )\n\n    @property\n    def name(self):\n        \"\"\"Return the name of the sensor.\"\"\"\n        return self._name\n\n    @property\n    def unique_id(self):\n        \"\"\"Return uniqueid.\"\"\"\n        return self._attr_unique_id\n\n    @property\n    def device_info(self):\n        return {\"name\": self._device_name, \"identifiers\": {self._device_identifier}}\n\n    @property\n    def native_value(self):\n        \"\"\"Return the state of the sensor.\"\"\"\n        sensor_value = self._handle._sensors.get(self._sensor_type)\n        if sensor_value is None:\n            return None\n        if self._sensor_type == ATTR_MONEY_SAVED:\n            return round(sensor_value, 2)\n        else:\n            return round(sensor_value, 3)\n\n    @property\n    def device_class(self):\n        \"\"\"Return the device class of the sensor.\"\"\"\n        return self._type_of_sensor\n\n    @property\n    def state_class(self):\n        \"\"\"Return the device class of the sensor.\"\"\"\n        if self._sensor_type in [\n            CHARGING_RATE,\n            DISCHARGING_RATE,\n            SOLAR_POWER_CAP,\n            ATTR_LAST_CHARGE_EFFICIENCY,\n            ATTR_LAST_DISCHARGE_EFFICIENCY,\n        ]:\n            return SensorStateClass.MEASUREMENT\n        return SensorStateClass.TOTAL\n\n    @property\n    def unit_of_measurement(self):\n        \"\"\"Return the unit the value is expressed in.\"\"\"\n        return self._units\n\n    @property\n    def extra_state_attributes(self):\n        \"\"\"Return the state attributes of the sensor.\"\"\"\n        state_attr = {}\n        for input in self._handle._inputs:\n            if self._sensor_type != input[SIMULATED_SENSOR]:\n                continue\n            if input[SENSOR_TYPE] == EXPORT:\n                continue\n            parent_sensor = input[SENSOR_ID]\n            if self.hass.states.get(parent_sensor) is None or self.hass.states.get(\n                parent_sensor\n            ).state in [STATE_UNAVAILABLE, STATE_UNKNOWN]:\n                continue\n            real_world_value = float(self.hass.states.get(parent_sensor).state)\n            simulated_value = self._handle._sensors[self._sensor_type]\n            if real_world_value == 0:\n                _LOGGER.warning(\n                    \"Division by zero, real world: %s, simulated: %s, battery: %s\",\n                    real_world_value,\n                    simulated_value,\n                    self._name,\n                )\n                state_attr = {PERCENTAGE_ENERGY_IMPORT_SAVED: 0}\n            else:\n                percentage_value_saved = (\n                    100 * (real_world_value - simulated_value) / real_world_value\n                )\n                state_attr = {\n                    PERCENTAGE_ENERGY_IMPORT_SAVED: round(\n                        float(percentage_value_saved), 0\n                    )\n                }\n            break\n        return state_attr\n\n    @property\n    def icon(self):\n        \"\"\"Return the icon to use in the frontend, if any.\"\"\"\n\n    @property\n    def state(self):\n        \"\"\"Return the state of the sensor.\"\"\"\n        sensor_value = self._handle._sensors.get(self._sensor_type)\n        if sensor_value is None:\n            return None\n        if self._sensor_type in [\n            ATTR_MONEY_SAVED,\n            ATTR_MONEY_SAVED_EXPORT,\n            ATTR_MONEY_SAVED_IMPORT,\n        ]:\n            return round(sensor_value, 2)\n        else:\n            return round(sensor_value, 3)\n\n    def update(self):\n        \"\"\"Not used.\"\"\"\n        return\n\n    @property\n    def last_reset(self):\n        \"\"\"Return the time when the sensor was last reset.\"\"\"\n        if not self._supports_last_reset:\n            return None\n        return self._last_reset\n\n    @property\n    def available(self) -> bool:\n        \"\"\"Needed to avoid spikes in energy dashboard on startup.\n        Return True if entity is available.\n        \"\"\"\n        return self._available\n\n\nclass SimulatedBattery(RestoreEntity, SensorEntity):\n    \"\"\"Representation of the battery itself.\"\"\"\n\n    _attr_should_poll = False\n\n    def __init__(self, handle):\n        self.handle = handle\n        self._date_recording_started = time.asctime()\n        self._name = f\"{handle._name}\"\n        self._attr_unique_id = f\"{handle._name}\"\n\n    async def async_added_to_hass(self):\n        \"\"\"Handle entity which will be added.\"\"\"\n        await super().async_added_to_hass()\n\n        state = await self.async_get_last_state()\n        if state:\n            if state.state in _INVALID_RESTORED_STATES:\n                _LOGGER.debug(\n                    \"Ignoring invalid restored battery state '%s' for '%s'.\",\n                    state.state,\n                    self._name,\n                )\n            else:\n                try:\n                    self.handle._charge_state = min(\n                        float(state.state), self.handle.current_max_capacity\n                    )\n                except (TypeError, ValueError):\n                    _LOGGER.debug(\n                        \"Battery state '%s' not restored properly for '%s'.\",\n                        state.state,\n                        self._name,\n                    )\n            if ATTR_DATE_RECORDING_STARTED in state.attributes:\n                self.handle._date_recording_started = state.attributes[\n                    ATTR_DATE_RECORDING_STARTED\n                ]\n\n        async def async_update_state():\n            \"\"\"Update sensor state.\"\"\"\n            await self.async_update_ha_state(True)\n\n        async_dispatcher_connect(\n            self.hass, f\"{self._name}-{MESSAGE_TYPE_BATTERY_UPDATE}\", async_update_state\n        )\n\n    @property\n    def name(self):\n        \"\"\"Return the name of the sensor.\"\"\"\n        return self._name\n\n    @property\n    def unique_id(self):\n        \"\"\"Return uniqueid.\"\"\"\n        return self._attr_unique_id\n\n    @property\n    def device_info(self):\n        return {\n            \"name\": self._name,\n            \"identifiers\": {self.handle.device_identifier},\n        }\n\n    @property\n    def native_value(self):\n        \"\"\"Return the state of the sensor.\"\"\"\n        return round(float(self.handle._charge_state), 3)\n\n    @property\n    def device_class(self):\n        \"\"\"Return the device class of the sensor.\"\"\"\n        return SensorDeviceClass.ENERGY\n\n    @property\n    def state_class(self):\n        \"\"\"Return the device class of the sensor.\"\"\"\n        return SensorStateClass.MEASUREMENT\n\n    @property\n    def native_unit_of_measurement(self):\n        \"\"\"Return the unit the value is expressed in.\"\"\"\n        return UnitOfEnergy.KILO_WATT_HOUR\n\n    @property\n    def unit_of_measurement(self):\n        \"\"\"Return the unit the value is expressed in.\"\"\"\n        return UnitOfEnergy.KILO_WATT_HOUR\n\n    @property\n    def extra_state_attributes(self):\n        \"\"\"Return the state attributes of the sensor.\"\"\"\n        sensor_list = \"\"\n        for input in self.handle._inputs:\n            sensor_list = f\"{sensor_list}, {input[SENSOR_ID]}\"\n        return {\n            ATTR_STATUS: self.handle._sensors[BATTERY_MODE],\n            ATTR_CHARGE_PERCENTAGE: int(self.handle._charge_percentage),\n            ATTR_DATE_RECORDING_STARTED: self.handle._date_recording_started,\n            CONF_BATTERY_SIZE: self.handle._battery_size,\n            CONF_BATTERY_DISCHARGE_EFFICIENCY: self.handle._battery_discharge_efficiency,\n            CONF_BATTERY_CHARGE_EFFICIENCY: self.handle._battery_charge_efficiency,\n            CONF_BATTERY_EFFICIENCY: self.handle._battery_discharge_efficiency,\n            CONF_BATTERY_MAX_DISCHARGE_RATE: float(self.handle._max_discharge_rate),\n            CONF_BATTERY_MAX_CHARGE_RATE: float(self.handle._max_charge_rate),\n            CONF_RATED_BATTERY_CYCLES: float(self.handle._rated_battery_cycles),\n            CONF_END_OF_LIFE_DEGRADATION: float(self.handle._end_of_life_degradation),\n            ATTR_SOURCE_ID: sensor_list,\n        }\n\n    @property\n    def icon(self):\n        \"\"\"Return the icon to use in the frontend.\"\"\"\n        if self.handle._sensors[BATTERY_MODE] in [MODE_CHARGING, MODE_FORCE_CHARGING]:\n            return ICON_CHARGING\n        if self.handle._sensors[BATTERY_MODE] == MODE_FULL:\n            return ICON_FULL\n        if self.handle._sensors[BATTERY_MODE] == MODE_EMPTY:\n            return ICON_EMPTY\n        return ICON_DISCHARGING\n\n    @property\n    def state(self):\n        \"\"\"Return the state of the sensor.\"\"\"\n        return round(float(self.handle._charge_state), 3)\n\n\nclass BatteryStatus(SensorEntity):\n    \"\"\"Representation of the battery itself.\"\"\"\n\n    _attr_should_poll = False\n\n    def __init__(self, handle, sensor_name):\n        self.handle = handle\n        self._date_recording_started = time.asctime()\n        self._name = f\"{handle._name} \".replace(\"_\", \" \") + f\"{sensor_name}\".replace(\"_\", \" \").capitalize()\n        self._attr_unique_id = f\"{handle._name} - {sensor_name}\"\n        self._device_name = handle._name\n        self._device_identifier = handle.device_identifier\n        self._sensor_type = sensor_name\n\n    async def async_added_to_hass(self):\n        \"\"\"Handle entity which will be added.\"\"\"\n        await super().async_added_to_hass()\n\n        async def async_update_state():\n            \"\"\"Update sensor state.\"\"\"\n            await self.async_update_ha_state(True)\n\n        async_dispatcher_connect(\n            self.hass,\n            f\"{self._device_name}-{MESSAGE_TYPE_BATTERY_UPDATE}\",\n            async_update_state,\n        )\n\n    @property\n    def name(self):\n        \"\"\"Return the name of the sensor.\"\"\"\n        return self._name\n\n    @property\n    def unique_id(self):\n        \"\"\"Return uniqueid.\"\"\"\n        return self._attr_unique_id\n\n    @property\n    def device_info(self):\n        return {\"name\": self._device_name, \"identifiers\": {self._device_identifier}}\n\n    @property\n    def native_value(self):\n        \"\"\"Return the state of the sensor.\"\"\"\n        return self.handle._sensors[BATTERY_MODE]\n\n    @property\n    def device_class(self):\n        \"\"\"Return the device class of the sensor.\"\"\"\n        return SensorDeviceClass.ENUM\n\n    @property\n    def extra_state_attributes(self):\n        \"\"\"Return the state attributes of the sensor.\"\"\"\n        return {\n            ATTR_STATUS: self.handle._sensors.get(ATTR_STATUS),\n            ATTR_CHARGE_PERCENTAGE: getattr(self.handle, \"_charge_percentage\", None),\n        }\n\n    @property\n    def icon(self):\n        \"\"\"Return the icon to use in the frontend.\"\"\"\n        status = self.handle._sensors.get(ATTR_STATUS)\n        if status == MODE_FULL:\n            return ICON_FULL\n        if status == MODE_EMPTY:\n            return ICON_EMPTY\n        if self.handle._sensors[BATTERY_MODE] in [MODE_CHARGING, MODE_FORCE_CHARGING]:\n            return ICON_CHARGING\n        return ICON_DISCHARGING\n\n    @property\n    def state(self):\n        \"\"\"Return the state of the sensor.\"\"\"\n        return self.handle._sensors[BATTERY_MODE]\n"
  },
  {
    "path": "custom_components/battery_sim/services.yaml",
    "content": "set_battery_charge_state:\n  name: battery_sim.set_battery_charge_state.name\n  description: battery_sim.set_battery_charge_state.description\n  fields:\n    device_id:\n      name: battery_sim.set_battery_charge_state.fields.device_id.name\n      description: battery_sim.set_battery_charge_state.fields.device_id.description\n      required: true\n      selector:\n        device:\n          integration: battery_sim\n    charge_state:\n      name: battery_sim.set_battery_charge_state.fields.charge_state.name\n      description: battery_sim.set_battery_charge_state.fields.charge_state.description\n      required: true\n      selector:\n        number:\n          mode: box\n          min: 0\n          max: 10000\n          step: 0.1\n\nset_battery_cycles:\n  name: battery_sim.set_battery_cycles.name\n  description: battery_sim.set_battery_cycles.description\n  fields:\n    device_id:\n      name: battery_sim.set_battery_cycles.fields.device_id.name\n      description: battery_sim.set_battery_cycles.fields.device_id.description\n      required: true\n      selector:\n        device:\n          integration: battery_sim\n    battery_cycles:\n      name: battery_sim.set_battery_cycles.fields.battery_cycles.name\n      description: battery_sim.set_battery_cycles.fields.battery_cycles.description\n      required: true\n      selector:\n        number:\n          mode: box\n          min: 0\n          max: 50000\n          step: 1\n"
  },
  {
    "path": "custom_components/battery_sim/switch.py",
    "content": "\"\"\"Switch platform for Battery Sim.\"\"\"\nimport logging\n\nfrom homeassistant.components.switch import SwitchEntity\n\nfrom .const import DOMAIN, CONF_BATTERY, PAUSE_BATTERY\n\n_LOGGER = logging.getLogger(__name__)\n\nBATTERY_SWITCHES = [\n    {\n        \"name\": PAUSE_BATTERY,\n        \"key\": \"pause_battery_enabled\",\n        \"icon\": \"mdi:pause\",\n    }\n]\n\n\nasync def async_setup_entry(hass, config_entry, async_add_entities):\n    handle = hass.data[DOMAIN][config_entry.entry_id]\n\n    battery_switches = [\n        BatterySwitch(handle, switch[\"name\"], switch[\"key\"], switch[\"icon\"])\n        for switch in BATTERY_SWITCHES\n    ]\n    async_add_entities(battery_switches)\n\n    return True\n\n\nasync def async_setup_platform(\n    hass, configuration, async_add_entities, discovery_info=None\n):\n    if discovery_info is None:\n        _LOGGER.error(\"This platform is only available through discovery\")\n        return\n\n    for conf in discovery_info:\n        battery = conf[CONF_BATTERY]\n        handle = hass.data[DOMAIN][battery]\n\n    battery_switches = [\n        BatterySwitch(handle, switch[\"name\"], switch[\"key\"], switch[\"icon\"])\n        for switch in BATTERY_SWITCHES\n    ]\n    async_add_entities(battery_switches)\n    return True\n\n\nclass BatterySwitch(SwitchEntity):\n    \"\"\"Switch to pause or resume the simulated battery.\"\"\"\n\n    def __init__(self, handle, switch_type, key, icon):\n        self.handle = handle\n        self._key = key\n        self._icon = icon\n        self._switch_type = switch_type\n        self._device_name = handle._name\n        self._device_identifier = handle.device_identifier\n        self._name = (\n            f\"{handle._name} \".replace(\"_\", \" \")\n            + f\"{switch_type}\".replace(\"_\", \" \").capitalize()\n        )\n        self._attr_unique_id = f\"{handle._name} - {switch_type}\"\n        self._type = type\n\n    @property\n    def unique_id(self):\n        return self._attr_unique_id\n\n    @property\n    def name(self):\n        return self._name\n\n    @property\n    def device_info(self):\n        return {\n            \"name\": self._device_name,\n            \"identifiers\": {self._device_identifier},\n        }\n\n    @property\n    def icon(self):\n        return self._icon\n\n    @property\n    def is_on(self):\n        return self.handle._switches[self._switch_type]\n\n    async def async_turn_on(self, **kwargs):\n        self.handle._switches[self._switch_type] = True\n        self.handle.async_trigger_update()\n        self.schedule_update_ha_state(True)\n        return True\n\n    async def async_turn_off(self, **kwargs):\n        self.handle._switches[self._switch_type] = False\n        self.handle.async_trigger_update()\n        self.schedule_update_ha_state(True)\n        return True\n"
  },
  {
    "path": "custom_components/battery_sim/translations/de.json",
    "content": "{\n  \"config\": {\n    \"abort\": {\n      \"already_configured\": \"Dieses Gerät ist bereits konfiguriert.\"\n    },\n    \"error\": {\n      \"invalid_input\": \"Ungültige Eingabe\"\n    },\n    \"flow_title\": \"Neue simulierte Batterie einrichten\",\n    \"step\": {\n      \"user\": {\n        \"title\": \"Batterie auswählen\",\n        \"data\": {\n          \"battery\": \"Batteriemodell\"\n        },\n        \"description\": \"Wähle ein zu simulierendes Batteriemodell aus der Liste oder wähle 'Benutzerdefiniert', um eines zu erstellen.\"\n      },\n      \"custom\": {\n        \"title\": \"Benutzerdefinierte Batterie\",\n        \"data\": {\n          \"unique_name\": \"Eindeutiger Name\",\n          \"size_kwh\": \"Batteriegröße in kWh\",\n          \"max_discharge_rate_kw\": \"Maximale Entladerate in kW\",\n          \"max_charge_rate_kw\": \"Maximale Laderate in kW\",\n          \"discharge_efficiency\": \"Entladewirkungsgrad oder Kurve (z. B. 0.95 oder 0:0.9, 5:0.95)\",\n          \"charge_efficiency\": \"Ladewirkungsgrad oder Kurve (z. B. 0.95 oder 0:0.9, 5:0.95)\",\n          \"rated_battery_cycles\": \"Nennzyklen der Batterie\",\n          \"end_of_life_degradation\": \"Kapazität am Ende der Lebensdauer (0 bis 1)\",\n          \"update_frequency\": \"Aktualisierungsintervall in Sekunden\",\n          \"solar_energy_sensor\": \"Solarenergiezählersensor\",\n          \"nominal_inverter_power_kw\": \"Nennleistung des Wechselrichters in kW\"\n        },\n        \"description\": \"Lege die Spezifikationen der Batterie fest. Bitte erwäge, Details der von dir simulierten Batterie auf unserem GitHub zu veröffentlichen, damit wir sie zur Vorlagenliste hinzufügen können.\"\n      },\n      \"meter_menu\": {\n        \"title\": \"Zähler hinzufügen\",\n        \"menu_options\": {\n          \"add_import_meter\": \"Bezugszähler hinzufügen (misst Energie, die vom Netz ins Haus kommt)\",\n          \"add_export_meter\": \"Einspeisezähler hinzufügen (misst Energie, die vom Haus ins Netz geht)\",\n          \"all_done\": \"Alles erledigt\"\n        },\n        \"description\": \"Es sind mindestens ein Bezugs- und ein Einspeisezähler erforderlich. Zähler, die die Solarerzeugung direkt überwachen, sollten nicht hinzugefügt werden.\"\n      },\n      \"tariff_menu\": {\n        \"title\": \"Tarifart auswählen\",\n        \"menu_options\": {\n          \"no_tariff_info\": \"Kein Tarif für diesen Zähler\",\n          \"fixed_tariff\": \"Ein konstanter Festpreis für den Tarif\",\n          \"tariff_sensor\": \"Ein Sensor, der den Wert eines zeitlich variablen Tarifs enthält\"\n        },\n        \"description\": \"\"\n      },\n      \"add_import_meter\": {\n        \"title\": \"Bezugszähler zur Batterie hinzufügen\",\n        \"data\": {\n          \"sensor_id\": \"Energiezählersensor\"\n        },\n        \"description\": \"Zählersensor auswählen\"\n      },\n      \"add_export_meter\": {\n        \"title\": \"Einspeisezähler zur Batterie hinzufügen\",\n        \"data\": {\n          \"sensor_id\": \"Energiezählersensor\"\n        },\n        \"description\": \"Zählersensor auswählen\"\n      },\n      \"fixed_tariff\": {\n        \"title\": \"Details zum Festpreistarif hinzufügen\",\n        \"data\": {\n          \"fixed_tariff\": \"Fester Tarifwert (falls zutreffend)\"\n        },\n        \"description\": \"\"\n      },\n      \"tariff_sensor\": {\n        \"title\": \"Details zum Tarifsensor hinzufügen\",\n        \"data\": {\n          \"tariff_sensor\": \"Sensor, der den aktuellen Tarif anzeigt (falls zutreffend)\"\n        },\n        \"description\": \"\"\n      }\n    }\n  },\n  \"options\": {\n    \"step\": {\n      \"init\": {\n        \"title\": \"Batterie-Optionen\",\n        \"description\": \"Zu ändernde Parameter auswählen\",\n        \"menu_options\": {\n          \"main_params\": \"Hauptparameter\",\n          \"input_sensors\": \"Zähler/Sensoren bearbeiten\",\n          \"all_done\": \"Alles erledigt\"\n        }\n      },\n      \"main_params\": {\n        \"title\": \"Haupt-Batterieoptionen\",\n        \"description\": \"Hauptparameter\",\n        \"data\": {\n          \"size_kwh\": \"Batteriegröße in kWh\",\n          \"max_discharge_rate_kw\": \"Maximale Entladerate in kW\",\n          \"max_charge_rate_kw\": \"Maximale Laderate in kW\",\n          \"discharge_efficiency\": \"Entladewirkungsgrad oder Kurve (z. B. 0.95 oder 0:0.9, 5:0.95)\",\n          \"charge_efficiency\": \"Ladewirkungsgrad oder Kurve (z. B. 0.95 oder 0:0.9, 5:0.95)\",\n          \"rated_battery_cycles\": \"Nennzyklen der Batterie\",\n          \"end_of_life_degradation\": \"Kapazität am Ende der Lebensdauer (0 bis 1)\",\n          \"update_frequency\": \"Aktualisierungsintervall in Sekunden\",\n          \"solar_energy_sensor\": \"Solarenergiezählersensor\",\n          \"nominal_inverter_power_kw\": \"Nennleistung des Wechselrichters in kW\"\n        }\n      },\n      \"input_sensors\": {\n        \"title\": \"Zähler/Sensoren bearbeiten\",\n        \"menu_options\": {\n          \"add_import_meter\": \"Bezugszähler hinzufügen (misst Energie, die vom Netz ins Haus kommt)\",\n          \"add_export_meter\": \"Einspeisezähler hinzufügen (misst Energie, die vom Haus ins Netz geht)\",\n          \"edit_input_tariff\": \"Tarifdetails für einen Zähler bearbeiten\",\n          \"delete_input\": \"Einen Zähler löschen\"\n        },\n        \"description\": \"Es sind mindestens ein Bezugs- und ein Einspeisezähler erforderlich. Zähler, die die Solarerzeugung direkt überwachen, sollten nicht verwendet werden.\"\n      },\n      \"tariff_menu\": {\n        \"title\": \"Tarifart auswählen\",\n        \"menu_options\": {\n          \"no_tariff_info\": \"Kein Tarif für diesen Zähler\",\n          \"fixed_tariff\": \"Ein konstanter Festpreis für den Tarif\",\n          \"tariff_sensor\": \"Ein Sensor, der den Wert eines zeitlich variablen Tarifs darstellt\"\n        },\n        \"description\": \"\"\n      },\n      \"add_import_meter\": {\n        \"title\": \"Bezugszähler zur Batterie hinzufügen\",\n        \"data\": {\n          \"sensor_id\": \"Energiezählersensor\"\n        },\n        \"description\": \"Zählersensor auswählen\"\n      },\n      \"add_export_meter\": {\n        \"title\": \"Einspeisezähler zur Batterie hinzufügen\",\n        \"data\": {\n          \"sensor_id\": \"Energiezählersensor\"\n        },\n        \"description\": \"Zählersensor auswählen\"\n      },\n      \"delete_input\": {\n        \"title\": \"Zu löschenden Zähler auswählen\",\n        \"data\": {},\n        \"description\": \"\"\n      },\n      \"edit_input_tariff\": {\n        \"title\": \"Zähler auswählen, für den der Tarif bearbeitet werden soll\",\n        \"data\": {},\n        \"description\": \"\"\n      },\n      \"fixed_tariff\": {\n        \"title\": \"Festpreistarif hinzufügen\",\n        \"data\": {\n          \"fixed_tariff\": \"Fester Tarifwert\"\n        },\n        \"description\": \"\"\n      },\n      \"tariff_sensor\": {\n        \"title\": \"Tarifsensor hinzufügen\",\n        \"data\": {\n          \"tariff_sensor\": \"Sensor, der den aktuellen Tarif anzeigt\"\n        },\n        \"description\": \"\"\n      }\n    }\n  },\n  \"services\": {\n    \"set_battery_charge_state\": {\n      \"name\": \"Batterieladung einstellen\",\n      \"description\": \"Stellt den Ladezustand für eine bestimmte simulierte Batterie ein\",\n      \"fields\": {\n        \"device_id\": {\n          \"name\": \"Ziel-Batteriegerät\",\n          \"description\": \"Gerät, für das die Batterieladung eingestellt werden soll\"\n        },\n        \"charge_state\": {\n          \"name\": \"Ladezustand\",\n          \"description\": \"Ladungswert in kWh\"\n        }\n      }\n    },\n    \"set_battery_cycles\": {\n      \"name\": \"Batteriezyklen einstellen\",\n      \"description\": \"Stellt die simulierten Batteriezyklen für ein bestimmtes Batteriegerät ein\",\n      \"fields\": {\n        \"device_id\": {\n          \"name\": \"Ziel-Batteriegerät\",\n          \"description\": \"Gerät, für das die Batteriezyklen eingestellt werden sollen\"\n        },\n        \"battery_cycles\": {\n          \"name\": \"Batteriezyklen\",\n          \"description\": \"Anzuwendende Anzahl Zyklen\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "custom_components/battery_sim/translations/en.json",
    "content": "{\n  \"config\": {\n    \"abort\": {\n      \"already_configured\": \"This device is already configured.\"\n    },\n    \"error\": {\n      \"invalid_input\": \"Invalid input\"\n    },\n    \"flow_title\": \"Setup new simulated battery\",\n    \"step\": {\n      \"user\": {\n        \"title\": \"Select battery\",\n        \"data\": {\n          \"battery\": \"Battery model\"\n        },\n        \"description\": \"Select a battery model to simulate from the list or select Custom to create one.\"\n      },\n      \"custom\": {\n        \"title\": \"Custom Battery\",\n        \"data\": {\n          \"unique_name\": \"Unique name\",\n          \"size_kwh\": \"Battery size in kWh\",\n          \"max_discharge_rate_kw\": \"Maximum discharge rate in kW\",\n          \"max_charge_rate_kw\": \"Maximum charging rate in kW\",\n          \"discharge_efficiency\": \"Discharge efficiency or curve (e.g. 0.95 or 0:0.9, 5:0.95)\",\n          \"charge_efficiency\": \"Charge efficiency or curve (e.g. 0.95 or 0:0.9, 5:0.95)\",\n          \"rated_battery_cycles\": \"Rated battery cycles\",\n          \"end_of_life_degradation\": \"Capacity at end of life (0 to 1)\",\n          \"update_frequency\": \"Update frequency in seconds\",\n          \"solar_energy_sensor\": \"Solar energy sensor\",\n          \"nominal_inverter_power_kw\": \"Nominal inverter power in kW\"\n        },\n        \"description\": \"Set the specifications of the battery. Efficiencies can be entered as a single value or as power/efficiency pairs such as 0:0.9, 5:0.95. The optional solar energy sensor caps charging to the solar energy produced during each update interval. Please consider posting details of the battery you are simulating on our github so we can add it to the template list.\"\n      },\n      \"meter_menu\": {\n        \"title\": \"Add Meters\",\n        \"menu_options\": {\n          \"add_import_meter\": \"Add import meter (measuring energy coming into home from the grid)\",\n          \"add_export_meter\": \"Add export meter (measuring energy leaving into home to the grid)\",\n          \"all_done\": \"All finished\"\n        },\n        \"description\": \"At least one import and one export meter are required. Meters monitoring solar generation directly shouldn't be added.\"\n      },\n      \"tariff_menu\": {\n        \"title\": \"Select Tariff Type\",\n        \"menu_options\": {\n          \"no_tariff_info\": \"No tariff for this meter\",\n          \"fixed_tariff\": \"A constant fixed number for the tariff\",\n          \"tariff_sensor\": \"A sensor that contains the value of a tariff varying over time\"\n        },\n        \"description\": \"\"\n      },\n      \"add_import_meter\": {\n        \"title\": \"Add Import Meter To Battery\",\n        \"data\": {\n          \"sensor_id\": \"Energy meter sensor\"\n        },\n        \"description\": \"Select meter sensor\"\n      },\n      \"add_export_meter\": {\n        \"title\": \"Add Export Meter To Battery\",\n        \"data\": {\n          \"sensor_id\": \"Energy meter sensor\"\n        },\n        \"description\": \"Select meter sensor\"\n      },\n      \"fixed_tariff\": {\n        \"title\": \"Add Fixed Tariff Details\",\n        \"data\": {\n          \"fixed_tariff\": \"Fixed tariff value (if applicable)\"\n        },\n        \"description\": \"\"\n      },\n      \"tariff_sensor\": {\n        \"title\": \"Add Tariff Sensor Details\",\n        \"data\": {\n          \"tariff_sensor\": \"Sensor that shows current tariff (if applicable)\"\n        },\n        \"description\": \"\"\n      }\n    }\n  },\n  \"options\": {\n    \"step\": {\n      \"init\": {\n        \"title\": \"Battery Options\",\n        \"description\": \"Select parameters to amend\",\n        \"menu_options\": {\n          \"main_params\": \"Main Parameters\",\n          \"input_sensors\": \"Edit Meters/Sensors\",\n          \"all_done\": \"All done\"\n        }\n      },\n      \"main_params\": {\n        \"title\": \"Main Battery Options\",\n        \"description\": \"Main Parameters\",\n        \"data\": {\n          \"size_kwh\": \"Battery size in kWh\",\n          \"max_discharge_rate_kw\": \"Maximum discharge rate in kW\",\n          \"max_charge_rate_kw\": \"Maximum charging rate in kW\",\n          \"discharge_efficiency\": \"Discharge efficiency or curve (e.g. 0.95 or 0:0.9, 5:0.95)\",\n          \"charge_efficiency\": \"Charge efficiency or curve (e.g. 0.95 or 0:0.9, 5:0.95)\",\n          \"rated_battery_cycles\": \"Rated battery cycles\",\n          \"end_of_life_degradation\": \"Capacity at end of life (0 to 1)\",\n          \"update_frequency\": \"Update frequency in seconds\",\n          \"solar_energy_sensor\": \"Solar energy sensor\",\n          \"nominal_inverter_power_kw\": \"Nominal inverter power in kW\"\n        }\n      },\n      \"input_sensors\": {\n        \"title\": \"Edit Meters/Sensors\",\n        \"menu_options\": {\n          \"add_import_meter\": \"Add import meter (measuring energy coming into home from the grid)\",\n          \"add_export_meter\": \"Add export meter (measuring energy leaving home to the grid)\",\n          \"edit_input_tariff\": \"Edit tariff details for a meter\",\n          \"delete_input\": \"Delete a meter\"\n        },\n        \"description\": \"At least one import and one export meter are required. Meters monitoring solar generation directly shouldn't be used.\"\n      },\n      \"tariff_menu\": {\n        \"title\": \"Select Tariff Type\",\n        \"menu_options\": {\n          \"no_tariff_info\": \"No tariff for this meter\",\n          \"fixed_tariff\": \"A constant fixed price for the tariff\",\n          \"tariff_sensor\": \"A sensor that represents the value of a tariff varying over time\"\n        },\n        \"description\": \"\"\n      },\n      \"add_import_meter\": {\n        \"title\": \"Add Import Meter To Battery\",\n        \"data\": {\n          \"sensor_id\": \"Energy meter sensor\"\n        },\n        \"description\": \"Select meter sensor\"\n      },\n      \"add_export_meter\": {\n        \"title\": \"Add Export Meter To Battery\",\n        \"data\": {\n          \"sensor_id\": \"Energy meter sensor\"\n        },\n        \"description\": \"Select meter sensor\"\n      },\n      \"delete_input\": {\n        \"title\": \"Select Meter To Delete\",\n        \"data\": {},\n        \"description\": \"\"\n      },\n      \"edit_input_tariff\": {\n        \"title\": \"Select Meter To Edit Tariff For\",\n        \"data\": {},\n        \"description\": \"\"\n      },\n      \"fixed_tariff\": {\n        \"title\": \"Add Fixed Tariff\",\n        \"data\": {\n          \"fixed_tariff\": \"Fixed tariff value\"\n        },\n        \"description\": \"\"\n      },\n      \"tariff_sensor\": {\n        \"title\": \"Add Tariff Sensor\",\n        \"data\": {\n          \"tariff_sensor\": \"Sensor that shows current tariff\"\n        },\n        \"description\": \"\"\n      }\n    }\n  },\n  \"services\": {\n    \"set_battery_charge_state\": {\n      \"name\": \"Set Battery Charge\",\n      \"description\": \"Set the battery charge state for a specific simulated battery\",\n      \"fields\": {\n        \"device_id\": {\n          \"name\": \"Target Battery Device\",\n          \"description\": \"Device to set battery charge for\"\n        },\n        \"charge_state\": {\n          \"name\": \"Charge State\",\n          \"description\": \"Value of charge in kWh\"\n        }\n      }\n    },\n    \"set_battery_cycles\": {\n      \"name\": \"Set Battery Cycles\",\n      \"description\": \"Set the simulated battery cycles for a specific battery device\",\n      \"fields\": {\n        \"device_id\": {\n          \"name\": \"Target Battery Device\",\n          \"description\": \"Device to set battery cycles for\"\n        },\n        \"battery_cycles\": {\n          \"name\": \"Battery Cycles\",\n          \"description\": \"Cycle count to apply\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "custom_components/battery_sim/translations/nl.json",
    "content": "{\n  \"config\": {\n    \"abort\": {\n      \"already_configured\": \"Het apparaat is al geconfigureerd.\"\n    },\n    \"error\": {\n      \"invalid_input\": \"Ongeldige invoer\"\n    },\n    \"flow_title\": \"Nieuwe gesimuleerde batterij instellen\",\n    \"step\": {\n      \"user\": {\n        \"title\": \"Selecteer batterij\",\n        \"data\": {\n          \"battery\": \"Batterijmodel\"\n        },\n        \"description\": \"Selecteer een batterijmodel om te simuleren uit de lijst of kies Aangepast om er zelf een te maken.\"\n      },\n      \"custom\": {\n        \"title\": \"Aangepaste batterij\",\n        \"data\": {\n          \"unique_name\": \"Unieke naam\",\n          \"size_kwh\": \"Batterijgrootte in kWh\",\n          \"max_discharge_rate_kw\": \"Maximale ontlaadsnelheid in kW\",\n          \"max_charge_rate_kw\": \"Maximale laadsnelheid in kW\",\n          \"discharge_efficiency\": \"Ontlaadefficiëntie of curve (bijv. 0.95 of 0:0.9, 5:0.95)\",\n          \"charge_efficiency\": \"Laadefficiëntie of curve (bijv. 0.95 of 0:0.9, 5:0.95)\",\n          \"rated_battery_cycles\": \"Nominale batterijcycli\",\n          \"end_of_life_degradation\": \"Capaciteit aan het einde van de levensduur (0 t/m 1)\",\n          \"update_frequency\": \"Updatefrequentie in seconden\",\n          \"solar_energy_sensor\": \"Solarenergie sensor\",\n          \"nominal_inverter_power_kw\": \"Nominaal vermogen van de omvormer in kW\"\n        },\n        \"description\": \"Stel de specificaties van de batterij in. Efficiënties kunnen worden ingevoerd als een enkele waarde of als combinaties van vermogen/efficiëntie zoals 0:0.9, 5:0.95. Overweeg om details van de batterij die je simuleert op onze GitHub te delen, zodat we die aan de sjabloonlijst kunnen toevoegen.\"\n      },\n      \"meter_menu\": {\n        \"title\": \"Meters toevoegen\",\n        \"menu_options\": {\n          \"add_import_meter\": \"Importmeter toevoegen (meet energie die vanuit het net het huis binnenkomt)\",\n          \"add_export_meter\": \"Exportmeter toevoegen (meet energie die vanuit het huis naar het net gaat)\",\n          \"all_done\": \"Alles klaar\"\n        },\n        \"description\": \"Er is minimaal één importmeter en één exportmeter vereist. Meters die direct zonne-opwek meten, moeten niet worden toegevoegd.\"\n      },\n      \"tariff_menu\": {\n        \"title\": \"Selecteer tarieftype\",\n        \"menu_options\": {\n          \"no_tariff_info\": \"Geen tarief voor deze meter\",\n          \"fixed_tariff\": \"Een vaste constante waarde voor het tarief\",\n          \"tariff_sensor\": \"Een sensor die de waarde bevat van een tarief dat in de tijd varieert\"\n        },\n        \"description\": \"\"\n      },\n      \"add_import_meter\": {\n        \"title\": \"Importmeter aan batterij toevoegen\",\n        \"data\": {\n          \"sensor_id\": \"Energiemetersensor\"\n        },\n        \"description\": \"Selecteer metersensor\"\n      },\n      \"add_export_meter\": {\n        \"title\": \"Exportmeter aan batterij toevoegen\",\n        \"data\": {\n          \"sensor_id\": \"Energiemetersensor\"\n        },\n        \"description\": \"Selecteer metersensor\"\n      },\n      \"fixed_tariff\": {\n        \"title\": \"Details vast tarief toevoegen\",\n        \"data\": {\n          \"fixed_tariff\": \"Vaste tariefwaarde (indien van toepassing)\"\n        },\n        \"description\": \"\"\n      },\n      \"tariff_sensor\": {\n        \"title\": \"Details tariefsensors toevoegen\",\n        \"data\": {\n          \"tariff_sensor\": \"Sensor die het huidige tarief toont (indien van toepassing)\"\n        },\n        \"description\": \"\"\n      }\n    }\n  },\n  \"options\": {\n    \"step\": {\n      \"init\": {\n        \"title\": \"Batterijopties\",\n        \"description\": \"Selecteer parameters om aan te passen\",\n        \"menu_options\": {\n          \"main_params\": \"Hoofdparameters\",\n          \"input_sensors\": \"Meters/sensoren bewerken\",\n          \"all_done\": \"Alles klaar\"\n        }\n      },\n      \"main_params\": {\n        \"title\": \"Belangrijkste batterijopties\",\n        \"description\": \"Hoofdparameters\",\n        \"data\": {\n          \"size_kwh\": \"Batterijgrootte in kWh\",\n          \"max_discharge_rate_kw\": \"Maximale ontlaadsnelheid in kW\",\n          \"max_charge_rate_kw\": \"Maximale laadsnelheid in kW\",\n          \"discharge_efficiency\": \"Ontlaadefficiëntie of curve (bijv. 0.95 of 0:0.9, 5:0.95)\",\n          \"charge_efficiency\": \"Laadefficiëntie of curve (bijv. 0.95 of 0:0.9, 5:0.95)\",\n          \"rated_battery_cycles\": \"Nominale batterijcycli\",\n          \"end_of_life_degradation\": \"Capaciteit aan het einde van de levensduur (0 tot 1)\",\n          \"update_frequency\": \"Updatefrequentie in seconden\",\n          \"solar_energy_sensor\": \"Solarenergie sensor\",\n          \"nominal_inverter_power_kw\": \"Nominaal vermogen van de omvormer in kW\"\n        }\n      },\n      \"input_sensors\": {\n        \"title\": \"Meters/sensoren bewerken\",\n        \"menu_options\": {\n          \"add_import_meter\": \"Importmeter toevoegen (meet energie die vanuit het net het huis binnenkomt)\",\n          \"add_export_meter\": \"Exportmeter toevoegen (meet energie die vanuit het huis naar het net gaat)\",\n          \"edit_input_tariff\": \"Tariefdetails voor een meter bewerken\",\n          \"delete_input\": \"Een meter verwijderen\"\n        },\n        \"description\": \"Er is minimaal één importmeter en één exportmeter vereist. Meters die direct zonne-opwek meten, moeten niet worden gebruikt.\"\n      },\n      \"tariff_menu\": {\n        \"title\": \"Selecteer tarieftype\",\n        \"menu_options\": {\n          \"no_tariff_info\": \"Geen tarief voor deze meter\",\n          \"fixed_tariff\": \"Een vaste constante prijs voor het tarief\",\n          \"tariff_sensor\": \"Een sensor die de waarde weergeeft van een tarief dat in de tijd varieert\"\n        },\n        \"description\": \"\"\n      },\n      \"add_import_meter\": {\n        \"title\": \"Importmeter aan batterij toevoegen\",\n        \"data\": {\n          \"sensor_id\": \"Energiemetersensor\"\n        },\n        \"description\": \"Selecteer metersensor\"\n      },\n      \"add_export_meter\": {\n        \"title\": \"Exportmeter aan batterij toevoegen\",\n        \"data\": {\n          \"sensor_id\": \"Energiemetersensor\"\n        },\n        \"description\": \"Selecteer metersensor\"\n      },\n      \"delete_input\": {\n        \"title\": \"Selecteer meter om te verwijderen\",\n        \"data\": {},\n        \"description\": \"\"\n      },\n      \"edit_input_tariff\": {\n        \"title\": \"Selecteer meter waarvan je het tarief wilt bewerken\",\n        \"data\": {},\n        \"description\": \"\"\n      },\n      \"fixed_tariff\": {\n        \"title\": \"Vast tarief toevoegen\",\n        \"data\": {\n          \"fixed_tariff\": \"Vaste tariefwaarde\"\n        },\n        \"description\": \"\"\n      },\n      \"tariff_sensor\": {\n        \"title\": \"Tariefsensors toevoegen\",\n        \"data\": {\n          \"tariff_sensor\": \"Sensor die het huidige tarief toont\"\n        },\n        \"description\": \"\"\n      }\n    }\n  },\n  \"services\": {\n    \"set_battery_charge_state\": {\n      \"name\": \"Batterijlading instellen\",\n      \"description\": \"Stelt de laadstatus van de gesimuleerde batterij in voor een specifiek apparaat\",\n      \"fields\": {\n        \"device_id\": {\n          \"name\": \"Doelbatterijapparaat\",\n          \"description\": \"Apparaat waarvoor de batterijlading moet worden ingesteld\"\n        },\n        \"charge_state\": {\n          \"name\": \"Laadstatus\",\n          \"description\": \"Laadwaarde in kWh\"\n        }\n      }\n    },\n    \"set_battery_cycles\": {\n      \"name\": \"Batterijcycli instellen\",\n      \"description\": \"Stelt de gesimuleerde batterijcycli in voor een specifiek batterijapparaat\",\n      \"fields\": {\n        \"device_id\": {\n          \"name\": \"Doelbatterijapparaat\",\n          \"description\": \"Apparaat waarvoor de batterijcycli moeten worden ingesteld\"\n        },\n        \"battery_cycles\": {\n          \"name\": \"Batterijcycli\",\n          \"description\": \"Aantal cycli om toe te passen\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "custom_components/battery_sim/translations/sv.json",
    "content": "{\n  \"config\": {\n    \"abort\": {\n      \"already_configured\": \"Den här enheten är redan konfigurerad.\"\n    },\n    \"error\": {\n      \"invalid_input\": \"Ogiltig inmatning\"\n    },\n    \"flow_title\": \"Ställ in nytt simulerat batteri\",\n    \"step\": {\n      \"user\": {\n        \"title\": \"Välj batteri\",\n        \"data\": {\n          \"battery\": \"Batterimodell\"\n        },\n        \"description\": \"Välj en batterimodell att simulera från listan eller välj Anpassad för att skapa en ny.\"\n      },\n      \"custom\": {\n        \"title\": \"Anpassat batteri\",\n        \"data\": {\n          \"unique_name\": \"Unikt namn\",\n          \"size_kwh\": \"Batteristorlek i kWh\",\n          \"max_discharge_rate_kw\": \"Maximal urladdningshastighet i kW\",\n          \"max_charge_rate_kw\": \"Maximal laddningshastighet i kW\",\n          \"discharge_efficiency\": \"Urladdningseffektivitet eller kurva (t.ex. 0.95 eller 0:0.9, 5:0.95)\",\n          \"charge_efficiency\": \"Laddningseffektivitet eller kurva (t.ex. 0.95 eller 0:0.9, 5:0.95)\",\n          \"rated_battery_cycles\": \"Nominella battericykler\",\n          \"end_of_life_degradation\": \"Kapacitet vid slutet av livslängden (0 till 1)\",\n          \"update_frequency\": \"Uppdateringsfrekvens i sekunder\",\n          \"solar_energy_sensor\": \"Solenergisensor\",\n          \"nominal_inverter_power_kw\": \"Inverterarens nominella effekt, kW\"\n        },\n        \"description\": \"Ange batteriets specifikationer. Effektiviteter kan anges som ett enskilt värde eller som effekt/effektivitetspar som 0:0.9, 5:0.95. Överväg gärna att dela detaljer om batteriet du simulerar på vår GitHub så att vi kan lägga till det i mallistan.\"\n      },\n      \"meter_menu\": {\n        \"title\": \"Lägg till mätare\",\n        \"menu_options\": {\n          \"add_import_meter\": \"Lägg till importmätare (mäter energi som kommer in i hemmet från elnätet)\",\n          \"add_export_meter\": \"Lägg till exportmätare (mäter energi som lämnar hemmet till elnätet)\",\n          \"all_done\": \"Allt klart\"\n        },\n        \"description\": \"Minst en importmätare och en exportmätare krävs. Mätare som övervakar solelproduktion direkt ska inte läggas till.\"\n      },\n      \"tariff_menu\": {\n        \"title\": \"Välj tarifftyp\",\n        \"menu_options\": {\n          \"no_tariff_info\": \"Ingen tariff för den här mätaren\",\n          \"fixed_tariff\": \"Ett konstant fast värde för tariffen\",\n          \"tariff_sensor\": \"En sensor som innehåller värdet för en tariff som varierar över tid\"\n        },\n        \"description\": \"\"\n      },\n      \"add_import_meter\": {\n        \"title\": \"Lägg till importmätare till batteriet\",\n        \"data\": {\n          \"sensor_id\": \"Energimätarsensor\"\n        },\n        \"description\": \"Välj mätarsensor\"\n      },\n      \"add_export_meter\": {\n        \"title\": \"Lägg till exportmätare till batteriet\",\n        \"data\": {\n          \"sensor_id\": \"Energimätarsensor\"\n        },\n        \"description\": \"Välj mätarsensor\"\n      },\n      \"fixed_tariff\": {\n        \"title\": \"Lägg till detaljer för fast tariff\",\n        \"data\": {\n          \"fixed_tariff\": \"Fast tariffvärde (om tillämpligt)\"\n        },\n        \"description\": \"\"\n      },\n      \"tariff_sensor\": {\n        \"title\": \"Lägg till detaljer för tariffsensor\",\n        \"data\": {\n          \"tariff_sensor\": \"Sensor som visar aktuell tariff (om tillämpligt)\"\n        },\n        \"description\": \"\"\n      }\n    }\n  },\n  \"options\": {\n    \"step\": {\n      \"init\": {\n        \"title\": \"Batteriinställningar\",\n        \"description\": \"Välj parametrar att ändra\",\n        \"menu_options\": {\n          \"main_params\": \"Huvudparametrar\",\n          \"input_sensors\": \"Redigera mätare/sensorer\",\n          \"all_done\": \"Allt klart\"\n        }\n      },\n      \"main_params\": {\n        \"title\": \"Huvudsakliga batteriinställningar\",\n        \"description\": \"Huvudparametrar\",\n        \"data\": {\n          \"size_kwh\": \"Batteristorlek i kWh\",\n          \"max_discharge_rate_kw\": \"Maximal urladdningshastighet i kW\",\n          \"max_charge_rate_kw\": \"Maximal laddningshastighet i kW\",\n          \"discharge_efficiency\": \"Urladdningseffektivitet eller kurva (t.ex. 0.95 eller 0:0.9, 5:0.95)\",\n          \"charge_efficiency\": \"Laddningseffektivitet eller kurva (t.ex. 0.95 eller 0:0.9, 5:0.95)\",\n          \"rated_battery_cycles\": \"Nominella battericykler\",\n          \"end_of_life_degradation\": \"Kapacitet vid slutet av livslängden (0 till 1)\",\n          \"update_frequency\": \"Uppdateringsfrekvens i sekunder\",\n          \"solar_energy_sensor\": \"Solenergisensor\",\n          \"nominal_inverter_power_kw\": \"Inverterarens nominella effekt, kW\"\n        }\n      },\n      \"input_sensors\": {\n        \"title\": \"Redigera mätare/sensorer\",\n        \"menu_options\": {\n          \"add_import_meter\": \"Lägg till importmätare (mäter energi som kommer in i hemmet från elnätet)\",\n          \"add_export_meter\": \"Lägg till exportmätare (mäter energi som lämnar hemmet till elnätet)\",\n          \"edit_input_tariff\": \"Redigera tariffdetaljer för en mätare\",\n          \"delete_input\": \"Ta bort en mätare\"\n        },\n        \"description\": \"Minst en importmätare och en exportmätare krävs. Mätare som övervakar solelproduktion direkt ska inte användas.\"\n      },\n      \"tariff_menu\": {\n        \"title\": \"Välj tarifftyp\",\n        \"menu_options\": {\n          \"no_tariff_info\": \"Ingen tariff för den här mätaren\",\n          \"fixed_tariff\": \"Ett konstant fast pris för tariffen\",\n          \"tariff_sensor\": \"En sensor som representerar värdet för en tariff som varierar över tid\"\n        },\n        \"description\": \"\"\n      },\n      \"add_import_meter\": {\n        \"title\": \"Lägg till importmätare till batteriet\",\n        \"data\": {\n          \"sensor_id\": \"Energimätarsensor\"\n        },\n        \"description\": \"Välj mätarsensor\"\n      },\n      \"add_export_meter\": {\n        \"title\": \"Lägg till exportmätare till batteriet\",\n        \"data\": {\n          \"sensor_id\": \"Energimätarsensor\"\n        },\n        \"description\": \"Välj mätarsensor\"\n      },\n      \"delete_input\": {\n        \"title\": \"Välj mätare att ta bort\",\n        \"data\": {},\n        \"description\": \"\"\n      },\n      \"edit_input_tariff\": {\n        \"title\": \"Välj mätare vars tariff ska redigeras\",\n        \"data\": {},\n        \"description\": \"\"\n      },\n      \"fixed_tariff\": {\n        \"title\": \"Lägg till fast tariff\",\n        \"data\": {\n          \"fixed_tariff\": \"Fast tariffvärde\"\n        },\n        \"description\": \"\"\n      },\n      \"tariff_sensor\": {\n        \"title\": \"Lägg till tariffsensor\",\n        \"data\": {\n          \"tariff_sensor\": \"Sensor som visar aktuell tariff\"\n        },\n        \"description\": \"\"\n      }\n    }\n  },\n  \"services\": {\n    \"set_battery_charge_state\": {\n      \"name\": \"Ange batteriladdning\",\n      \"description\": \"Ställ in batteriets laddningsnivå för en specifik simulerad enhet\",\n      \"fields\": {\n        \"device_id\": {\n          \"name\": \"Målbatterienhet\",\n          \"description\": \"Enhet vars batteriladdning ska ställas in\"\n        },\n        \"charge_state\": {\n          \"name\": \"Laddningsnivå\",\n          \"description\": \"Laddningsvärde i kWh\"\n        }\n      }\n    },\n    \"set_battery_cycles\": {\n      \"name\": \"Ange battericykler\",\n      \"description\": \"Ställ in simulerade battericykler för en specifik batterienhet\",\n      \"fields\": {\n        \"device_id\": {\n          \"name\": \"Målbatterienhet\",\n          \"description\": \"Enhet vars battericykler ska ställas in\"\n        },\n        \"battery_cycles\": {\n          \"name\": \"Battericykler\",\n          \"description\": \"Antal cykler att tillämpa\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "hacs.json",
    "content": "{\n  \"name\": \"Battery Simulator\",\n  \"render_readme\": true\n}\n"
  },
  {
    "path": "scripts/check_translations_usage.py",
    "content": "#!/usr/bin/env python3\n\"\"\"Check translation key usage and duplicate JSON keys for battery_sim.\"\"\"\n\nfrom __future__ import annotations\n\nimport ast\nimport json\nimport pathlib\nimport re\nimport sys\nfrom typing import Any\n\nROOT = pathlib.Path(__file__).resolve().parents[1]\nCOMPONENT = ROOT / \"custom_components\" / \"battery_sim\"\nTRANSLATIONS = COMPONENT / \"translations\"\nCONFIG_FLOW = COMPONENT / \"config_flow.py\"\nCONSTS = COMPONENT / \"const.py\"\n\n\ndef parse_consts() -> dict[str, str]:\n    consts: dict[str, str] = {}\n    for line in CONSTS.read_text(encoding=\"utf-8\").splitlines():\n        match = re.match(r'([A-Z_]+)\\s*=\\s*\"([^\"]+)\"', line)\n        if match:\n            consts[match.group(1)] = match.group(2)\n    return consts\n\n\ndef resolve_const(name: str, consts: dict[str, str]) -> str:\n    return consts.get(name, name)\n\n\ndef flatten_leaves(obj: Any, prefix: tuple[str, ...] = ()) -> set[tuple[str, ...]]:\n    leaves: set[tuple[str, ...]] = set()\n    if isinstance(obj, dict):\n        for key, value in obj.items():\n            leaves |= flatten_leaves(value, prefix + (key,))\n    else:\n        leaves.add(prefix)\n    return leaves\n\n\ndef load_json_and_duplicates(path: pathlib.Path) -> tuple[dict[str, Any], list[str]]:\n    duplicates: list[str] = []\n\n    def pairs_hook(pairs: list[tuple[str, Any]]) -> dict[str, Any]:\n        out: dict[str, Any] = {}\n        seen: set[str] = set()\n        for key, value in pairs:\n            if key in seen:\n                duplicates.append(key)\n            seen.add(key)\n            out[key] = value\n        return out\n\n    data = json.loads(path.read_text(encoding=\"utf-8\"), object_pairs_hook=pairs_hook)\n    return data, duplicates\n\n\ndef collect_used_paths() -> set[tuple[str, ...]]:\n    consts = parse_consts()\n    src = CONFIG_FLOW.read_text(encoding=\"utf-8\")\n    tree = ast.parse(src)\n\n    step_ids: set[str] = set()\n\n    for node in ast.walk(tree):\n        if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute):\n            if node.func.attr in {\"async_show_form\", \"async_show_menu\"}:\n                for kw in node.keywords:\n                    if (\n                        kw.arg == \"step_id\"\n                        and isinstance(kw.value, ast.Constant)\n                        and isinstance(kw.value.value, str)\n                    ):\n                        step_ids.add(kw.value.value)\n\n    used: set[tuple[str, ...]] = {\n        (\"config\", \"abort\", \"already_configured\"),\n        (\"config\", \"flow_title\"),\n        (\"config\", \"error\", \"invalid_input\"),\n    }\n\n    for section in (\"config\", \"options\"):\n        for step_id in step_ids:\n            used.add((section, \"step\", step_id, \"title\"))\n            used.add((section, \"step\", step_id, \"description\"))\n\n    for opt in (\"add_import_meter\", \"add_export_meter\", \"all_done\"):\n        used.add((\"config\", \"step\", \"meter_menu\", \"menu_options\", opt))\n    for opt in (\"no_tariff_info\", \"fixed_tariff\", \"tariff_sensor\"):\n        used.add((\"config\", \"step\", \"tariff_menu\", \"menu_options\", opt))\n        used.add((\"options\", \"step\", \"tariff_menu\", \"menu_options\", opt))\n    for opt in (\"main_params\", \"input_sensors\", \"all_done\"):\n        used.add((\"options\", \"step\", \"init\", \"menu_options\", opt))\n    for opt in (\"add_import_meter\", \"add_export_meter\", \"edit_input_tariff\", \"delete_input\"):\n        used.add((\"options\", \"step\", \"input_sensors\", \"menu_options\", opt))\n\n    step_fields = {\n        (\"config\", \"user\"): [\"BATTERY_TYPE\"],\n        (\"config\", \"custom\"): [\n            \"CONF_UNIQUE_NAME\",\n            \"CONF_BATTERY_SIZE\",\n            \"CONF_BATTERY_MAX_DISCHARGE_RATE\",\n            \"CONF_BATTERY_MAX_CHARGE_RATE\",\n            \"CONF_BATTERY_DISCHARGE_EFFICIENCY\",\n            \"CONF_BATTERY_CHARGE_EFFICIENCY\",\n            \"CONF_RATED_BATTERY_CYCLES\",\n            \"CONF_END_OF_LIFE_DEGRADATION\",\n            \"CONF_UPDATE_FREQUENCY\",\n        ],\n        (\"config\", \"add_import_meter\"): [\"SENSOR_ID\"],\n        (\"config\", \"add_export_meter\"): [\"SENSOR_ID\"],\n        (\"config\", \"fixed_tariff\"): [\"FIXED_TARIFF\"],\n        (\"config\", \"tariff_sensor\"): [\"TARIFF_SENSOR\"],\n        (\"options\", \"main_params\"): [\n            \"CONF_BATTERY_SIZE\",\n            \"CONF_BATTERY_MAX_DISCHARGE_RATE\",\n            \"CONF_BATTERY_MAX_CHARGE_RATE\",\n            \"CONF_BATTERY_DISCHARGE_EFFICIENCY\",\n            \"CONF_BATTERY_CHARGE_EFFICIENCY\",\n            \"CONF_RATED_BATTERY_CYCLES\",\n            \"CONF_END_OF_LIFE_DEGRADATION\",\n            \"CONF_UPDATE_FREQUENCY\",\n        ],\n        (\"options\", \"add_import_meter\"): [\"SENSOR_ID\"],\n        (\"options\", \"add_export_meter\"): [\"SENSOR_ID\"],\n        (\"options\", \"fixed_tariff\"): [\"FIXED_TARIFF\"],\n        (\"options\", \"tariff_sensor\"): [\"TARIFF_SENSOR\"],\n        (\"options\", \"delete_input\"): [\"CONF_INPUT_LIST\"],\n        (\"options\", \"edit_input_tariff\"): [\"CONF_INPUT_LIST\"],\n    }\n\n    for (section, step_id), fields in step_fields.items():\n        for field in fields:\n            used.add((section, \"step\", step_id, \"data\", resolve_const(field, consts)))\n\n    for svc in (\"set_battery_charge_state\", \"set_battery_cycles\"):\n        used.add((\"services\", svc, \"name\"))\n        used.add((\"services\", svc, \"description\"))\n\n    for svc, field in (\n        (\"set_battery_charge_state\", \"device_id\"),\n        (\"set_battery_charge_state\", \"charge_state\"),\n        (\"set_battery_cycles\", \"device_id\"),\n        (\"set_battery_cycles\", \"battery_cycles\"),\n    ):\n        used.add((\"services\", svc, \"fields\", field, \"name\"))\n        used.add((\"services\", svc, \"fields\", field, \"description\"))\n\n    return used\n\n\ndef main() -> int:\n    used = collect_used_paths()\n    any_problem = False\n\n    for path in sorted(TRANSLATIONS.glob(\"*.json\")):\n        data, duplicates = load_json_and_duplicates(path)\n        leaves = flatten_leaves(data)\n        unused = sorted(\".\".join(p) for p in leaves - used)\n        missing = sorted(\".\".join(p) for p in used - leaves)\n\n        print(f\"{path.name}:\")\n        print(f\"  duplicate keys: {len(duplicates)}\")\n        print(f\"  unused leaf keys: {len(unused)}\")\n        print(f\"  missing used keys: {len(missing)}\")\n\n        if duplicates:\n            any_problem = True\n            print(\"  duplicate key names:\")\n            for d in duplicates:\n                print(f\"    - {d}\")\n\n        if unused:\n            any_problem = True\n            print(\"  unused keys:\")\n            for u in unused:\n                print(f\"    - {u}\")\n\n        if missing:\n            any_problem = True\n            print(\"  missing keys:\")\n            for m in missing:\n                print(f\"    - {m}\")\n\n    return 1 if any_problem else 0\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n"
  }
]