diff --git a/.gitignore b/.gitignore index 76a29a7..0176dba 100644 --- a/.gitignore +++ b/.gitignore @@ -160,4 +160,18 @@ cython_debug/ #.idea/ nordpool.py -brm.py \ No newline at end of file +brm.py + +# Ignore files specific to VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9b38853 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "python.testing.pytestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} \ No newline at end of file diff --git a/README.md b/README.md index 8724332..7471520 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# PV Opt: Home Assistant Solar/Battery Optimiser v3.15.5 +# PV Opt: Home Assistant Solar/Battery Optimiser v3.16.1

This documentation needs updating!

@@ -9,6 +9,8 @@ The application will integrate fully with Solis inverters which are controlled u - [Home Assistant Solax Modbus Integration](https://github.com/wills106/homeassistant-solax-modbus) - [Home Assistant Core Modbus Integration](https://github.com/fboundy/ha_solis_modbus) - [Home Assistant Solarman Integration](https://github.com/StephanJoubert/home_assistant_solarman) +- [Home Assistant Solis Sensor Integration](https://github.com/hultenvp/solis-sensor) (read-only mode) +- [Home Assistant Solis Control Integration](https://github.com/stevegal/solis_control) (allows inverter control via solis_cloud and HA automations) Once installed it should require miminal configuration. Other inverters/integrations can be added if required or can be controlled indirectly using automations. @@ -49,7 +51,7 @@ This app is not stand-alone it requires the following:

2. Install HACS

1. Install HACS: https://hacs.xyz/docs/setup/download -2. Enable AppDaemon in HACS: https://hacs.xyz/docs/categories/appdaemon_apps/ +2. Enable AppDaemon in HACS: https://hacs.xyz/docs/use/repositories/type/appdaemon/#making-appdaemon-apps-visible-in-hacs

3. Install the Solcast PV Solar Integration (v4.0.x)

@@ -67,7 +69,9 @@ This excellent integration will pull Octopus Price data in to Home Assistant. So

5. Install the Integration to Control Your Inverter

-At present this app only works directly with Solis hybrid inverters using either the Solax Modbus integration (https://github.com/wills106/homeassistant-solax-modbus) or the HA Core Modbus as described here: https://github.com/fboundy/ha_solis_modbus. Support for the Solarman integration (https://github.com/StephanJoubert/home_assistant_solarman) is in test. At the moment writing to the inverter is disabled pending further testing by Solarman users. +At present this app only works directly with Solis hybrid inverters using either the Solax Modbus integration (https://github.com/wills106/homeassistant-solax-modbus), the HA Core Modbus as described here: https://github.com/fboundy/ha_solis_modbus, or combining the [Solis-Senor](https://github.com/hultenvp/solis-sensor) and [Solis-Control](https://github.com/hultenvp/solis_control) integrations. + +Support for the Solarman integration (https://github.com/StephanJoubert/home_assistant_solarman) is in test. At the moment writing to the inverter is disabled pending further testing by Solarman users.

Solax Modbus:

@@ -88,6 +92,15 @@ At present this app only works directly with Solis hybrid inverters using either Follow the Github instructions here: https://github.com/fboundy/ha_solis_modbus +

Using Solis Cloud

+
Solis-Sensor
+ +Follow the Github instruction here: https://github.com/hultenvp/solis-sensor + +
Solis-Control
+ +Follow the Github instruction here: https://github.com/hultenvp/solis_control +

Solarman

Follow the Github instructions here: https://github.com/StephanJoubert/home_assistant_solarman @@ -101,9 +114,7 @@ Follow the Github instructions here: https://github.com/StephanJoubert/home_assi

7. Install Mosquitto MQTT Broker

-1. Click the Home Assistant My button below to open the add-on on your Home Assistant instance: - - [![](https://camo.githubusercontent.com/c16bd5d7acfc6d5163636b546783e9217e27a401c1ac5bfd93a2ef5fa23e15fe/68747470733a2f2f6d792e686f6d652d617373697374616e742e696f2f6261646765732f73757065727669736f725f6164646f6e2e737667)](https://my.home-assistant.io/redirect/supervisor_addon/?addon=a0d7b954_appdaemon&repository_url=https%3A%2F%2Fgithub.com%2Fhassio-addons%2Frepository) +1. Navigate to Settings -> Addons and click "Mosquito Broker" 2. Click on Install @@ -113,6 +124,10 @@ Follow the Github instructions here: https://github.com/StephanJoubert/home_assi

8. Install File Editor

+Follow instructions here: https://github.com/home-assistant/addons/blob/master/configurator/README.md + +Navigate to Settings -> Addons -> File editor -> Configuration and set "Enforce Basepath" to "off". +

9. Install Samba Share and/or Studio Code Server Add-ons If Required

Both of these add-ons make it easier to edit text files on your HA Install but aren't strictly necessary. `Samba Share` also makes it easier to access the AppDaemon log files. @@ -125,7 +140,7 @@ AppDaemon is a loosely coupled, multi-threaded, sandboxed python execution envir 1. Click the Home Assistant My button below to open the add-on on your Home Assistant instance: - [![](https://camo.githubusercontent.com/c16bd5d7acfc6d5163636b546783e9217e27a401c1ac5bfd93a2ef5fa23e15fe/68747470733a2f2f6d792e686f6d652d617373697374616e742e696f2f6261646765732f73757065727669736f725f6164646f6e2e737667)](http://homeassistant.local:8123/hassio/addon/core_mosquitto/info) + [![](https://camo.githubusercontent.com/a54868bd2c4edb2d623ab2fef3d074fe711b45c2c1cdc0fbe4dfd296faa594f8/68747470733a2f2f6d792e686f6d652d617373697374616e742e696f2f6261646765732f73757065727669736f725f6164646f6e2e737667)](https://my.home-assistant.io/redirect/supervisor_addon/?addon=a0d7b954_appdaemon&repository_url=https%3A%2F%2Fgithub.com%2Fhassio-addons%2Frepository) 2. Click on Install @@ -166,6 +181,11 @@ AppDaemon is a loosely coupled, multi-threaded, sandboxed python execution envir api: hadashboard: +And add the `client_user` and `client_password` keys to `secrets.yaml` like this: + + mqtt-user: some_user + mqtt-password: some_password + 3. It is also recommended that you add the following entries to `appdaemon.yaml` to improve AppDaemon logging. These settings assume that you have a `/share/logs` folder setup using `Samba Share`. logs: @@ -303,6 +323,126 @@ Restarts between Home Assistant and Add-Ons are not synchronised so it is helpfu addon: a0d7b954_appdaemon mode: single +

14. For Solis-Control: Add Automation to Control Inverter

+ +If you're using the solis-sensor and solis_control integrations through Solis Cloud, you'll need to add the following automation which will send the messages to Solis Cloud in order to control your inverter. N.B: It's important that you've set up the solis_control integration correctly and requested API access via Solis Cloud Technical Support. + +``` +alias: "Solis: Use PV_Opt" +description: "Use the output of pv_opt to control your Solis inverter via Solis Cloud." +trigger: + - platform: state + entity_id: + - sensor.pvopt_status + to: Idle (Read Only) + for: + hours: 0 + minutes: 0 + seconds: 5 + enabled: false + - platform: time_pattern + hours: /1 + minutes: "00" + seconds: "05" + - platform: time_pattern + hours: /1 + minutes: "30" + seconds: "05" + - platform: state + entity_id: + - sensor.pvopt_charge_start +condition: [] +action: + - if: + - condition: template + value_template: >- + {{ states('sensor.pvopt_charge_start') | as_datetime | as_local <= + today_at("23:59") }} + then: + - data: + days: + - chargeCurrent: >- + {% set direction = float(states('sensor.pvopt_charge_current'), + 0.0) %} {% set chargeAmps = min((max(direction, 0.0) | + round(method='floor')), 50)%} {{ chargeAmps }} + dischargeCurrent: >- + {% set direction = float(states('sensor.pvopt_charge_current'), + 0.0) %} {% set dischargeAmps = min((min(direction, 0.0) | abs | + round(method='floor')), 50) %} {{ dischargeAmps }} + chargeStartTime: >- + {% set direction = float(states('sensor.pvopt_charge_current'), + 0.0) %} {% set startChargeTime = '00:00' %} {% if direction >= + 0.0 -%} + {% set startChargeTime = (as_local(as_datetime(states('sensor.pvopt_charge_start')))|string)[11:16] %} + {%- endif %} {{ startChargeTime }} + chargeEndTime: >- + {% set direction = float(states('sensor.pvopt_charge_current'), + 0.0) %} {% set endChargeTime = '00:00' %} {% if direction >= 0.0 + -%} + {% set endChargeTime = (as_local(as_datetime(states('sensor.pvopt_charge_end')))|string)[11:16] %} + {%- endif %} {{ endChargeTime }} + dischargeStartTime: >- + {% set direction = float(states('sensor.pvopt_charge_current'), + 0.0) %} {% set startDischargeTime = '00:00' %} {% if direction < + 0.0 -%} + {% set startDischargeTime = (as_local(as_datetime(states('sensor.pvopt_charge_start')))|string)[11:16] %} + {%- endif %} {{ startDischargeTime }} + dischargeEndTime: >- + {% set direction = float(states('sensor.pvopt_charge_current'), + 0.0) %} {% set endDischargeTime = '00:00' %} {% if direction < + 0.0 -%} + {% set endDischargeTime = (as_local(as_datetime(states('sensor.pvopt_charge_end')))|string)[11:16] %} + {%- endif %} {{ endDischargeTime }} + - chargeCurrent: "0" + dischargeCurrent: "0" + chargeStartTime: "00:00" + chargeEndTime: "00:00" + dischargeStartTime: "00:00" + dischargeEndTime: "00:00" + - chargeCurrent: "0" + dischargeCurrent: "0" + chargeStartTime: "00:00" + chargeEndTime: "00:00" + dischargeStartTime: "00:00" + dischargeEndTime: "00:00" + config: + secret: <> + key_id: "<>" + username: <> + password: <> + plantId: "<>" + action: pyscript.solis_control + else: + - data: + days: + - chargeCurrent: "0" + dischargeCurrent: "0" + chargeStartTime: "00:00" + chargeEndTime: "00:00" + dischargeStartTime: "00:00" + dischargeEndTime: "00:00" + - chargeCurrent: "0" + dischargeCurrent: "0" + chargeStartTime: "00:00" + chargeEndTime: "00:00" + dischargeStartTime: "00:00" + dischargeEndTime: "00:00" + - chargeCurrent: "0" + dischargeCurrent: "0" + chargeStartTime: "00:00" + chargeEndTime: "00:00" + dischargeStartTime: "00:00" + dischargeEndTime: "00:00" + config: + secret: <> + key_id: "<>" + username: <> + password: <> + plantId: "<>" + action: pyscript.solis_control +mode: single +``` +

Configuration

@@ -394,7 +534,7 @@ The app always produces a Base forecast of future battery SOC and the associated If `Optimise Charging` is enabled, an optimsised charging plan is calculated and writtemt to `sensor.pvopt_opt_cost`. This will also include a list of forced charge and discharge windows. -The easiest way to control and visualise this is through the `pvopt_dashboard.yaml` Lovelace yaml file included in this repo. Note that you will need to manually paste this into a dashboard and edit the charts to use the correct Octopus Energy sensors: +The easiest way to control and visualise this is through the `dashboards/pvopt_dashboard.yaml` Lovelace yaml file included in this repo. If you're using the Solis Cloud integration, you can start with the `dashboards/pvopt_dashboard_solis_cloud.yaml`. Note that you will need to manually paste this into a dashboard and edit the charts to use the correct Octopus Energy sensors: ![Alt text](image-1.png) diff --git a/apps/pv_opt/config/config.yaml b/apps/pv_opt/config/config.yaml index eb84d15..ebe51e8 100644 --- a/apps/pv_opt/config/config.yaml +++ b/apps/pv_opt/config/config.yaml @@ -35,6 +35,28 @@ pv_opt: # If true the personal data will be redacted from the log files. # redact_personal_data_from_log: false + #======================================= + #Logging Category Control + #======================================= + #Defines Logging subjects to add to logfile + #If commented out, everything is logged + #Ignored if "debug" is set to False above + # + # S = Startup/Initialisation Logging + # T = Tariff loading Logging + # P = Power consumption history Logging + # C = Charge algorithm Logging + # D = Discharge algorithm Logging + # W = Charge/Discharge Windows Logging + # F = Power Flows Logging + # V = Power Flows debugging (extra verbose) + # I = inverter control/commands Logging + # E = EV debugging + + # Letters can be added to "debug_categories" in any order + + debug_categories: W + # ======================================== # Basic parameters # ======================================== @@ -71,28 +93,28 @@ pv_opt: # Consumption forecast parameters # ======================================== # - use_consumption_history: false - # consumption_history_days: 6 - # - daily_consumption_kwh: 17 - shape_consumption_profile: true - consumption_shape: - - hour: 0 - consumption: 300 - - hour: 0.5 - consumption: 200 - - hour: 6 - consumption: 150 - - hour: 8 - consumption: 500 - - hour: 15.5 - consumption: 500 - - hour: 17 - consumption: 750 - - hour: 22 - consumption: 750 - - hour: 24 - consumption: 300 + # use_consumption_history: false + # # consumption_history_days: 6 + # # + # daily_consumption_kwh: 17 + # shape_consumption_profile: true + # consumption_shape: + # - hour: 0 + # consumption: 300 + # - hour: 0.5 + # consumption: 200 + # - hour: 6 + # consumption: 150 + # - hour: 8 + # consumption: 500 + # - hour: 15.5 + # consumption: 500 + # - hour: 17 + # consumption: 750 + # - hour: 22 + # consumption: 750 + # - hour: 24 + # consumption: 300 # ======================================== # Octopus account parameters @@ -117,6 +139,22 @@ pv_opt: # octopus_import_tariff_code: E-1R-GO-VAR-22-10-14-N # octopus_export_tariff_code: E-1R-OUTGOING-LITE-FIX-12M-23-09-12-N + + # ======================================== + # EV parameters + # ======================================== + # + # If you have a Zappi Charger, uncomment next line to allow Pv_opt to detect car plugin status + # for use with the Octopus Intelligent Tariff and to allow EV consumption data to be read (if needed). + # + # ev_charger: Zappi # Default = None + # + # By default, Zappi is set to seen as part of the house load to prevent house battery discharge during car charging and allow discounting of EV consumption + # from house charging calculations. + # + # If your Zappi is not seen as part of the house load, uncomment next line to set to False. + # + # ev_part_of_house_load = False # Default = True # =============================================================================================================== # Brand / Integration Specific Config: SOLIS_SOLAX_MODBUS: https://github.com/wills106/homeassistant-solax-modbus @@ -253,15 +291,15 @@ pv_opt: # # - sensor.{device_name}_pv_power_2 id_solar_power: sensor.{device_name}_pv_total_power - alt_tariffs: - - name: Agile_Fix - octopus_import_tariff_code: E-1R-AGILE-23-12-06-G - octopus_export_tariff_code: E-1R-OUTGOING-FIX-12M-19-05-13-G - - - name: Eco7_Fix - octopus_import_tariff_code: E-2R-VAR-22-11-01-G - octopus_export_tariff_code: E-1R-OUTGOING-FIX-12M-19-05-13-G + # alt_tariffs: + # - name: Agile_Fix + # octopus_import_tariff_code: E-1R-AGILE-23-12-06-G + # octopus_export_tariff_code: E-1R-OUTGOING-FIX-12M-19-05-13-G + + # - name: Eco7_Fix + # octopus_import_tariff_code: E-2R-VAR-22-11-01-G + # octopus_export_tariff_code: E-1R-OUTGOING-FIX-12M-19-05-13-G - - name: Flux - octopus_import_tariff_code: E-1R-FLUX-IMPORT-23-02-14-G - octopus_export_tariff_code: E-1R-FLUX-EXPORT-23-02-14-G \ No newline at end of file + # - name: Flux + # octopus_import_tariff_code: E-1R-FLUX-IMPORT-23-02-14-G + # octopus_export_tariff_code: E-1R-FLUX-EXPORT-23-02-14-G diff --git a/apps/pv_opt/pv_opt.py b/apps/pv_opt/pv_opt.py index 2ac2468..094e3e5 100644 --- a/apps/pv_opt/pv_opt.py +++ b/apps/pv_opt/pv_opt.py @@ -12,7 +12,7 @@ from numpy import nan import re -VERSION = "3.15.5" +VERSION = "3.16.1" OCTOPUS_PRODUCT_URL = r"https://api.octopus.energy/v1/products/" @@ -183,7 +183,7 @@ "default": 4.0, "attributes": { "min": 0.0, - "max": 1.0, + "max": 10.0, "step": 0.5, "mode": "slider", }, @@ -1185,6 +1185,9 @@ def _load_args(self, items=None): self.yaml_config[item] = self.config[item] elif "id_" in item: + self.log(f">>> Test: {self.entity_exists('update.home_assistant_core_update')}") + for v in values: + self.log(f">>> {item} {v} {self.entity_exists(v)}") if min([self.entity_exists(v) for v in values]): if len(values) == 1: self.config[item] = values[0] @@ -2755,8 +2758,7 @@ def _check_tariffs_vs_bottlecap(self): df = pd.DataFrame( self.get_state_retry(self.bottlecap_entities[direction], attribute=("rates")) ).set_index("start")["value_inc_vat"] - df.index = pd.to_datetime(df.index) - df.index = df.index.tz_convert("UTC") + df.index = pd.to_datetime(df.index, utc=True) df *= 100 df = pd.concat( [ diff --git a/apps/pv_opt/pvpy.py b/apps/pv_opt/pvpy.py index d60cdb8..c9dd91b 100644 --- a/apps/pv_opt/pvpy.py +++ b/apps/pv_opt/pvpy.py @@ -306,7 +306,7 @@ def _get_agile_predict(self): return df = pd.DataFrame(r.json()[0]["prices"]).set_index("date_time") - df.index = pd.to_datetime(df.index).tz_convert("UTC") + df.index = pd.to_datetime(df.index, utc=True) return df["agile_pred"] def get_day_ahead(self, start): diff --git a/pvopt_control_card.yaml b/dashboards/pvopt_control_card.yaml similarity index 100% rename from pvopt_control_card.yaml rename to dashboards/pvopt_control_card.yaml diff --git a/pvopt_dashboard.yaml b/dashboards/pvopt_dashboard.yaml similarity index 100% rename from pvopt_dashboard.yaml rename to dashboards/pvopt_dashboard.yaml diff --git a/dashboards/pvopt_dashboard_solis_cloud.yaml b/dashboards/pvopt_dashboard_solis_cloud.yaml new file mode 100644 index 0000000..081ed9f --- /dev/null +++ b/dashboards/pvopt_dashboard_solis_cloud.yaml @@ -0,0 +1,666 @@ +views: + - theme: Backend-selected + title: PV Opt Default + path: pv_opt_default + subview: false + type: custom:masonry-layout + layout: + max_cols: 4 + reflow: true + icon: mdi:solar-power-variant + badges: [] + cards: + - type: vertical-stack + cards: + - type: custom:bar-card + title: Status + entities: + - entity: sensor.solis_ac_output_total_power + unit_of_measurement: W + target: '{{states(''sensor.solcast_forecast_today'')}}' + name: Solar + max: 5000 + icon: mdi:solar-power-variant + - entity: sensor.solis_total_consumption_power + name: Load + max: 5000 + - entity: sensor.solis_remaining_battery_capacity + name: SOC + max: 100 + icon: mdi:battery + - entity: sensor.solis_grid_import_power + name: Import Power + max: 5000 + min: 0 + - entity: sensor.solis_grid_export_power + name: Export Power + max: 5000 + min: 0 + - entity: sensor.solis_battery_power + name: Battery Charge/Discharge + max: 5000 + min: -5000 + - type: entities + entities: + - type: custom:template-entity-row + name: Inverter Time + state: >- + {{((as_local(as_datetime(states('sensor.solis_rtc'))))|string)[11:16]}} + - type: markdown + content: > +

Cost Summary (GBP)

+ + + | | Today | Tomorrow | Total | + + |:--|---:|---:|---:| + + |Base | {{'%8.2f' | + format(state_attr('sensor.pvopt_base_cost','cost_today')| float)}} + | {{'%0.2f' | + format(state_attr('sensor.pvopt_base_cost','cost_tomorrow')| + float)}} | {{'%0.2f' | format(states('sensor.pvopt_base_cost')| + float)}} | + + |Optimised | {{'%8.2f' | + format(state_attr('sensor.pvopt_opt_cost','cost_today')| float)}} + | {{'%0.2f' | + format(state_attr('sensor.pvopt_opt_cost','cost_tomorrow')| + float)}} | {{'%0.2f' | format(states('sensor.pvopt_opt_cost')| + float)}} | + + |Cost Saving | {{'%8.2f' | + format((state_attr('sensor.pvopt_base_cost','cost_today')| + float-state_attr('sensor.pvopt_opt_cost','cost_today')| float) | + round(2))}} |{{'%0.2f' | + format((state_attr('sensor.pvopt_base_cost','cost_tomorrow')| + float-state_attr('sensor.pvopt_opt_cost','cost_tomorrow')| float) + | round(2))}} | {{'%0.2f' | + format((states('sensor.pvopt_base_cost')| float - + states('sensor.pvopt_opt_cost')| float) | round(2)) }} | + + +

Optimisation Breakdown (GBP)

+ + + | | Cost || + + |:--|:--|:--| + + {% set x = state_attr("sensor.pvopt_opt_cost","Summary")%}{%for y + in x%}|{{y}}|{{('%0.2f' | + format(x[y]['cost']))}}|{{x[y]['Selected']}}| + + {%endfor%} + + + +

Charge Plan

+ + + | Start | | | End ||| Power ||| Start SOC ||| End SOC | Hold SOC + | + + |:-------|--|--|:---------|--|--|:--------:|--|--|:--------:|--|--|:----------:|:--|{% + for a in state_attr('sensor.pvopt_charge_start', 'windows') %} + + {% set tf = '%d-%b %H:%M %Z'%} | + {{as_local(as_datetime(a['start'])).strftime(tf)}} ||| + {{as_local(as_datetime(a['end'])).strftime(tf)}} ||| {{a['forced'] + | float | round(0)}}W ||| {{a['soc'] | float | round(1)}}% ||| + {{a['soc_end'] | float | round(1)}}% | {{a['hold_soc']}} + |{%endfor%} + title: PV Opt Results + - type: custom:stack-in-card + title: Optimised Charging + cards: + - type: entities + entities: + - entity: sensor.pvopt_status + name: Status + - type: markdown + content:

Control Parameters + - type: entities + entities: + - entity: number.pvopt_optimise_frequency_minutes + name: Optimiser Freq (mins) + - entity: switch.pvopt_read_only + name: Read Only Mode + - entity: switch.pvopt_include_export + name: Include Export + - type: conditional + conditions: + - condition: state + entity: switch.pvopt_include_export + state: 'on' + card: + type: entities + entities: + - entity: switch.pvopt_forced_discharge + name: Optimise Discharging + - type: conditional + conditions: + - condition: state + entity: switch.pvopt_forced_discharge + state: 'on' + - condition: state + entity: switch.pvopt_include_export + state: 'on' + card: + type: entities + entities: + - entity: switch.pvopt_allow_cyclic + name: Allow Cyclic Charge/Discharge + - type: markdown + content:

Solar + - type: entities + entities: + - entity: switch.pvopt_use_solar + name: Use Solar + - type: conditional + conditions: + - condition: state + entity: switch.pvopt_use_solar + state: 'on' + card: + type: entities + entities: + - entity: number.pvopt_solcast_confidence_level + name: Solcast Confidence Level + - type: markdown + content:

Consumption and EV + - type: entities + entities: + - entity: switch.pvopt_use_consumption_history + name: Use Consumption History + - type: conditional + conditions: + - condition: state + entity: switch.pvopt_use_consumption_history + state: 'on' + card: + type: entities + entities: + - entity: number.pvopt_consumption_history_days + name: Load History Days + - entity: number.pvopt_consumption_margin + name: Load Margin + - entity: number.pvopt_day_of_week_weighting + name: Weekday Weighting + - type: conditional + conditions: + - condition: state + entity: switch.pvopt_use_consumption_history + state: 'off' + card: + type: entities + entities: + - entity: number.pvopt_daily_consumption_kwh + name: Daily Consumption (kWh) + - entity: switch.pvopt_shape_consumption_profile + name: Shape Consumption Profile + - type: entities + entities: + - entity: select.pvopt_ev_charger + name: EV Charger + - type: conditional + conditions: + - condition: state + entity: select.pvopt_ev_charger + state_not: None + card: + type: entities + entities: + - entity: number.pvopt_ev_charger_power_watts + name: EV Charger Power + - entity: number.pvopt_ev_battery_capacity_kwh + name: EV Battery Capacity + - type: markdown + content:

System Parameters + - type: entities + entities: + - entity: number.pvopt_battery_capacity_wh + name: Battery Capacity + - entity: number.pvopt_inverter_power_watts + name: Inverter Power + - entity: number.pvopt_charger_power_watts + name: Charger Power + - entity: number.pvopt_inverter_efficiency_percent + name: Inverter Efficiency + - entity: number.pvopt_charger_efficiency_percent + name: Charger Efficiency + - entity: number.pvopt_battery_current_limit_amps + name: Battery Current Limit + - type: markdown + content: Tuning Parameters + - type: entities + entities: + - entity: number.pvopt_pass_threshold_p + name: Charge threshold (p) + - entity: number.pvopt_discharge_threshold_p + name: Discharge threshold (p) + - entity: number.pvopt_plunge_threshold_p_kwh + name: Plunge threshold (p/kWh) + - entity: number.pvopt_slot_threshold_p + name: Threshold per slot (p) + - entity: number.pvopt_forced_power_group_tolerance + name: Power Resolution + - type: custom:stack-in-card + cards: + - type: vertical-stack + cards: + - type: markdown + content: Results + - type: entities + entities: + - entity: sensor.solis_remaining_battery_capacity + name: Current Battery SOC + - type: custom:template-entity-row + name: Next Charge/Discharge Slot Start + state: >- + {{(as_local(as_datetime(states('sensor.pvopt_charge_start')))|string)[:16]}} + icon: mdi:timer-sand-complete + - type: custom:template-entity-row + name: Next Charge/Discharge Slot End + state: >- + {{(as_local(as_datetime(states('sensor.pvopt_charge_end')))|string)[:16]}} + icon: mdi:timer-sand-complete + - entity: sensor.pvopt_charge_current + name: Optimum Charge Current + - entity: sensor.solis_battery_current + name: Battery Current + state_color: true + show_header_toggle: false + - type: custom:apexcharts-card + apex_config: + chart: + height: 234px + yaxis: + - id: power + show: true + decimals: 0 + apex_config: + forceNiceScale: true + header: + show: true + show_states: true + colorize_states: true + title: Solar Forecasts vs Actual + graph_span: 2d + stacked: false + span: + start: day + series: + - entity: sensor.solis_total_consumption_power + float_precision: 0 + extend_to: now + group_by: + func: avg + duration: 30min + - entity: sensor.solis_ac_output_total_power + float_precision: 0 + extend_to: now + name: Actual + stroke_width: 1 + type: column + color: '#ff7f00' + group_by: + func: avg + duration: 30min + offset: +15min + show: + name_in_header: true + in_header: true + in_chart: true + legend_value: true + offset_in_name: false + - entity: sensor.pvopt_opt_cost + type: line + name: Forecast Consumption + color: yellow + opacity: 0.7 + stroke_width: 2 + offset: +15min + show: + in_header: false + legend_value: false + data_generator: | + return entity.attributes.consumption.map((entry) => { + return [new Date(entry.period_start), entry.consumption]; + }); + - entity: sensor.solcast_pv_forecast_forecast_today + type: area + name: '' + color: cyan + opacity: 0.1 + stroke_width: 0 + unit: W + show: + in_header: false + legend_value: false + offset_in_name: false + data_generator: | + return entity.attributes.detailedForecast.map((entry) => { + return [new Date(entry.period_start), entry.pv_estimate90*1000]; + }); + offset: +15min + - entity: sensor.solcast_pv_forecast_forecast_today + type: area + name: ' ' + color: '#1c1c1c' + opacity: 1 + stroke_width: 0 + unit: W + show: + in_header: false + legend_value: false + offset_in_name: false + data_generator: | + return entity.attributes.detailedForecast.map((entry) => { + return [new Date(entry.period_start), entry.pv_estimate10*1000]; + }); + offset: +15min + - entity: sensor.solcast_pv_forecast_forecast_today + type: line + name: Solcast + color: cyan + opacity: 1 + stroke_width: 3 + unit: W + show: + in_header: false + legend_value: false + offset_in_name: false + data_generator: | + return entity.attributes.detailedForecast.map((entry) => { + return [new Date(entry.period_start), entry.pv_estimate*1000]; + }); + offset: +15min + - entity: sensor.solcast_pv_forecast_forecast_tomorrow + type: area + name: Solcast + color: cyan + opacity: 0.1 + stroke_width: 0 + unit: W + show: + in_header: false + legend_value: false + offset_in_name: false + data_generator: | + return entity.attributes.detailedForecast.map((entry) => { + return [new Date(entry.period_start), entry.pv_estimate90*1000]; + }); + offset: +15min + - entity: sensor.solcast_pv_forecast_forecast_tomorrow + type: area + name: Solcast + color: '#1c1c1c' + opacity: 1 + stroke_width: 0 + unit: W + show: + in_header: false + offset_in_name: false + legend_value: false + data_generator: | + return entity.attributes.detailedForecast.map((entry) => { + return [new Date(entry.period_start), entry.pv_estimate10*1000]; + }); + offset: +15min + - entity: sensor.solcast_pv_forecast_forecast_tomorrow + type: line + name: Solcast + color: cyan + opacity: 1 + stroke_width: 3 + unit: W + show: + in_header: false + legend_value: false + offset_in_name: false + data_generator: | + return entity.attributes.detailedForecast.map((entry) => { + return [new Date(entry.period_start), entry.pv_estimate*1000]; + }); + offset: +15min + - type: custom:apexcharts-card + apex_config: + chart: + height: 234px + yaxis: + - id: soc + show: true + min: 0 + max: 100 + decimals: 0 + apex_config: + tickAmount: 5 + header: + show: true + show_states: true + colorize_states: true + title: Battery SOC Forecast vs Actual + graph_span: 2d + span: + start: day + series: + - entity: sensor.solis_remaining_battery_capacity + extend_to: now + name: Actual + stroke_width: 1 + type: area + color: '#ff7f00' + opacity: 0.4 + yaxis_id: soc + - entity: sensor.pvopt_opt_cost + type: line + name: Optimised + color: '#7f7fff' + opacity: 0.7 + stroke_width: 2 + unit: '%' + show: + in_header: false + legend_value: false + data_generator: | + return entity.attributes.soc.map((entry) => { + return [new Date(entry.period_start), entry.soc]; + }); + yaxis_id: soc + - entity: sensor.pvopt_base_cost + type: line + name: Initial + color: '#7fff7f' + opacity: 0.7 + stroke_width: 2 + unit: '%' + show: + in_header: false + legend_value: false + data_generator: | + return entity.attributes.soc.map((entry) => { + return [new Date(entry.period_start), entry.soc]; + }); + yaxis_id: soc + - type: custom:apexcharts-card + apex_config: + chart: + height: 230px + header: + show: true + show_states: true + colorize_states: true + title: Pricing and Forced Charge/Discharge + graph_span: 2d + yaxis: + - id: price + decimals: 0 + min: -10 + max: 70 + apex_config: + tickAmount: 8 + - id: charge + decimals: 0 + opposite: true + show: true + min: -4000 + max: 4000 + apex_config: + tickAmount: 8 + stacked: false + span: + start: day + series: + - entity: sensor.pvopt_opt_cost + type: column + name: Forced Charge + yaxis_id: charge + color: orange + opacity: 0.7 + stroke_width: 2 + unit: W + offset: '-15min' + show: + in_header: false + legend_value: false + offset_in_name: false + data_generator: | + return entity.attributes.forced.map((entry) => { + return [new Date(entry.period_start), entry.forced]; + }); + - entity: >- + event.octopus_energy_electricity_19m1337498_1610032016836_current_day_rates + yaxis_id: price + name: Historic Import Price + color: yellow + opacity: 1 + stroke_width: 3 + extend_to: now + unit: p/kWh + data_generator: | + return entity.attributes.rates.map((entry) => { + return [new Date(entry.start), entry.value_inc_vat*100]; + }); + offset: '-15min' + show: + in_header: false + legend_value: false + offset_in_name: false + - entity: >- + event.octopus_energy_electricity_19m1337498_1610032016836_export_current_day_rates + yaxis_id: price + name: Historic Export Price + color: cyan + opacity: 1 + stroke_width: 3 + extend_to: now + unit: p/kWh + data_generator: | + return entity.attributes.rates.map((entry) => { + return [new Date(entry.start), entry.value_inc_vat*100]; + }); + offset: '-15min' + show: + in_header: false + legend_value: false + offset_in_name: false + - entity: sensor.pvopt_opt_cost + type: line + name: Future Import Price + float_precision: 1 + color: white + opacity: 1 + stroke_width: 3 + extend_to: now + unit: p/kWh + offset: '-15min' + show: + in_header: true + legend_value: false + offset_in_name: false + data_generator: | + return entity.attributes.import.map((entry) => { + return [new Date(entry.period_start), entry.import]; + }); + yaxis_id: price + - entity: sensor.pvopt_opt_cost + float_precision: 1 + type: line + name: Future Export Price + color: green + opacity: 1 + stroke_width: 3 + extend_to: now + unit: p/kWh + offset: '-15min' + show: + in_header: true + legend_value: false + offset_in_name: false + data_generator: | + return entity.attributes.export.map((entry) => { + return [new Date(entry.period_start), entry.export]; + }); + yaxis_id: price + - type: custom:octopus-energy-rates-card + currentEntity: >- + event.octopus_energy_electricity_19m1337498_1610032016836_current_day_rates + futureEntity: >- + event.octopus_energy_electricity_19m1337498_1610032016836_next_day_rates + cols: 2 + hour12: false + showday: true + showpast: false + title: Octopus Import + unitstr: p + lowlimit: 15 + mediumlimit: 20 + highlimit: 30 + roundUnits: 2 + cheapest: true + multiplier: 100 + - type: custom:stack-in-card + cards: + - type: custom:apexcharts-card + apex_config: + chart: + height: 234px + header: + show: true + show_states: true + colorize_states: true + title: Optimised Daily Cost + graph_span: 7d + span: + start: day + offset: '-6d' + series: + - entity: sensor.pvopt_opt_cost_actual + name: Actual + extend_to: false + curve: stepline + float_precision: 2 + show: + legend_value: false + - entity: sensor.pvopt_opt_cost_current + name: Current Contract + curve: stepline + float_precision: 2 + extend_to: false + show: + legend_value: false + - entity: sensor.pvopt_opt_cost_eco7_fix + name: Eco7 / Fix + curve: stepline + float_precision: 2 + extend_to: false + show: + legend_value: false + - entity: sensor.pvopt_opt_cost_flux + curve: stepline + name: Flux + float_precision: 2 + show: + legend_value: false + extend_to: false diff --git a/pvopt_test_card.yaml b/dashboards/pvopt_test_card.yaml similarity index 100% rename from pvopt_test_card.yaml rename to dashboards/pvopt_test_card.yaml