├── .github └── workflows │ └── pull_requests.yml ├── .gitignore ├── LICENSE ├── README.md ├── custom_components └── checkwatt │ ├── __init__.py │ ├── config_flow.py │ ├── const.py │ ├── event.py │ ├── manifest.json │ ├── sensor.py │ ├── services.yaml │ ├── strings.json │ └── translations │ ├── en.json │ └── sv.json ├── hacs.json ├── images ├── basic_sensor_annual.png ├── basic_sensor_daily.png ├── configure_done.png ├── configure_step_1.png ├── configure_step_2.png ├── configure_step_3.png ├── detailed_sensor_annual.png ├── detailed_sensor_daily.png ├── dev_tools_states.png ├── energy_done.png ├── energy_step_1.png ├── energy_step_2.png ├── energy_step_3.png ├── energy_step_4.png ├── energy_step_5.png ├── energy_step_6.png ├── expert_sensor.png ├── ha_main.png ├── options_done.png ├── options_step_1.png ├── options_step_2.png └── options_step_3.png └── setup.cfg /.github/workflows/pull_requests.yml: -------------------------------------------------------------------------------- 1 | name: Pull requests 2 | 3 | on: pull_request 4 | 5 | concurrency: 6 | group: pull-requests-${{ github.ref }} 7 | cancel-in-progress: true 8 | 9 | jobs: 10 | lint: 11 | name: "Linting" 12 | runs-on: "ubuntu-latest" 13 | steps: 14 | - name: "Clone repo" 15 | uses: actions/checkout@v3 16 | 17 | - name: "Set up Python" 18 | uses: actions/setup-python@v4 19 | with: 20 | python-version: '3.10' 21 | 22 | - name: "Install Python dependencies" 23 | run: pip install black flake8 24 | 25 | - name: "Run linters" 26 | uses: wearerequired/lint-action@v2 27 | with: 28 | auto_fix: false 29 | black: true 30 | flake8: true 31 | 32 | - name: "Hassfest" 33 | uses: "home-assistant/actions/hassfest@master" 34 | 35 | validate: 36 | name: "Validate hacs" 37 | runs-on: "ubuntu-latest" 38 | steps: 39 | - name: "Clone repo" 40 | uses: actions/checkout@v3 41 | 42 | - name: "HACS validation" 43 | uses: hacs/action@main 44 | with: 45 | category: "integration" 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Other 132 | SyncToy* 133 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Marcus Karlsson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Home Assistant CheckWatt Custom Integration 2 | This integration uses cloud polling from the CheckWatt EnergyInbalance portal using a reverse engineered private API. 3 | 4 | The strategy for this integration is to reduce the amount of sensors published while at the same time maximize the information available and provide them as attributes. 5 | 6 | The integration also aims to balance the user needs for information with loads on CheckWatts servers why some data may not always be up-to date. 7 | 8 | Out-of-the box, this integration provides two sensors: 9 | - CheckWatt Daily Net Income : Your estimated net income today 10 | - CheckWatt Annual Net Income : Your estimated annual net income 11 | - Battery State of Charge: Your batterys state of charge 12 | 13 | The Daily Net Income sensor also have an attribute that provides information about tomorrows estimated Net Income. 14 | 15 | ![checkwatt main](/images/ha_main.png) 16 | 17 | # Known Issues and Limitations 18 | 1. The integration uses undocumented API's used by the EnergyInBalance portal. These can change at any time and render this integration useless without any prior notice. 19 | 2. The monetary information from EnergyInBalance is always provisional and can change before you receive your invoice from CheckWatt. 20 | 3. The annual yeild sensor includes today's and tomorrow's estimated net income. 21 | 4. CheckWatt EnergyInBalance does not (always) provide Grid In/Out information. Why it is recommended to use other data sources for your Energy Panel. 22 | 5. The FCR-D state and status is pulled from a logbook parameter within the API and is a very fragile piece of information. Use with care. 23 | 6. The integration will update the Energy sensors once per minute, the Daily Net Income sensor once every fifteenth minute and the Annual Net Income around 2 am every morning. This to not put too much stress on the CheckWatt servers (the net income operation is slow resource heavy) 24 | 25 | # Installation 26 | ### HACS installation 27 | [![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=faanskit&repository=ha-checkwatt&category=integration) 28 | 29 | 30 | ### Git installation 31 | 1. Make sure you have git installed on your machine. 32 | 2. Navigate to you home assistant configuration folder. 33 | 3. Create a `custom_components` folder of it does not exist, navigate down into it after creation. 34 | 4. Execute the following command: `git clone https://github.com/faanskit/ha-checkwatt.git checkwatt` 35 | 5. Restart Home-Assistant. 36 | 37 | ## Enable the integration 38 | Go to Settings / Devices & Services / Integrations. Click **+ ADD INTEGRATION** 39 | 40 | Find CheckWatt from the list of available brands: 41 | 42 | ![checkwatt config step 1](/images/configure_step_1.png) 43 | 44 | Enter your EnergyInBalance credentials and press **SUBMIT**: 45 | 46 | Take note that Home Assistant will store your credentials and if this is a security concern for you, abort at this stage. 47 | 48 | ![checkwatt config step 2](/images/configure_step_2.png) 49 | 50 | The integration will now install and assuming it all went well you will get a success message and the possibility to add its sensors to an area of your choice. 51 | 52 | ![checkwatt config step 3](/images/configure_step_3.png) 53 | 54 | On your Overview you will now have two new sensors in a new group: 55 | 56 | ![checkwatt config done](/images/configure_done.png) 57 | 58 | These sensors will show you your estimated daily and annual net income alongside with some basic attributes like Street Address, Zip Code and City. 59 | 60 | The Daily Net Income sensor will also show tomorrows estimated net income as an attribute. 61 | 62 | ![checkwatt basic daily](/images/basic_sensor_daily.png) 63 | ![checkwatt basic annual](/images/basic_sensor_annual.png) 64 | 65 | 66 | ## Configuration 67 | The integration provides basic sensors for most peoples needs. The integration can also be a one-stop-shop for the Home Assistant Energy panel and can therefore be configured to also fetch all required data for that from EnergyInBalance. 68 | 69 | For those who need additional information, detailed attributes can be provided by configuring the integration accordingly. Through the detailed sensors you can get gross revenue, fees, fee rates, FCR-D status and much more. 70 | 71 | If you need more sensor and more detailed attributes in the sensors, configure the integration as follows. 72 | 73 | Go to Settings / Devices & Services / CheckWatt. Click **CONFIGURE**: 74 | 75 | ![checkwatt options step 1](/images/options_step_1.png) 76 | 77 | Select if you want the integration to provide energy sensors and if detailed attributes shall be provided. 78 | 79 | Press **SUBMIT** and the configurations will be stored: 80 | 81 | ![checkwatt options step 2](/images/options_step_2.png) 82 | 83 | After the configuration is done you need to restart the integration. Click **...** and select **Reload** 84 | ![checkwatt options step 3](/images/options_step_3.png) 85 | 86 | After the system as reload you will have 1 device and 9 sensors available. 87 | ![checkwatt options done](/images/options_done.png) 88 | 89 | Your sensors will now also contain a lot of detailed attributes. 90 | ![checkwatt detailed daily](/images/detailed_sensor_daily.png) 91 | ![checkwatt detailed annual](/images/detailed_sensor_annual.png) 92 | 93 | ## Setting up Energy Panel 94 | With the energy sensors provided by the integration, it is possible to configure the Home Assistant Energy Panel. The Energy panel is available on the left-hand menu of Home Assistant by default. 95 | 96 | When you enter the energy panel the first time you will be guided by a Wizard: 97 | 98 | ![checkwatt energy step 1](/images/energy_step_1.png) 99 | 100 | For the Grid Consumption, select the Import Energy sensor from the integration and complement it with the cost tracker using the *Spot Price VAT* sensors. 101 | 102 | **Take note** that you should use the sensor that includes VAT for Electricity that you purchase. Please **also note** that this sensor does not include markups from your electricity provider. 103 | 104 | ![checkwatt energy step 2](/images/energy_step_2.png) 105 | 106 | For Grid Consumption, select the Export Energy sensor from the integration and complement it with the cost tracker using the *Spot Price* sensors. 107 | 108 | **Take note** that you should use the sensor that excludes VAT for Electricity that you sell. 109 | 110 | ![checkwatt energy step 3](/images/energy_step_3.png) 111 | 112 | When configured, it looks like this. Press **SAVE + NEXT** to continue the Wizard. 113 | 114 | ![checkwatt energy step 4](/images/energy_step_4.png) 115 | 116 | Add the Solar Energy sensor from the integration and press **SAVE + NEXT** 117 | 118 | ![checkwatt energy step 5](/images/energy_step_5.png) 119 | 120 | Add the Battery Energy sensors from the integration. 121 | *Energy going in to the battery* = Battery Charging Energy Sensor 122 | *Energy going out of the battery* = Battery Discharging Energy Sensor 123 | 124 | Press **SAVE + NEXT** 125 | 126 | ![checkwatt energy step 6](/images/energy_step_6.png) 127 | 128 | Finish off the Wizard and the result will look like this. 129 | Please be advised that it will take a few hours before the Energy Panels start showing data. 130 | 131 | Also, currently EnergyInBalance does not always provide proper Grid Input and Output why this data cannot be relied upon. 132 | 133 | For now, you need to pull that data from another integration. 134 | 135 | ![checkwatt energy done](/images/energy_done.png) 136 | 137 | ## Final result 138 | When the system is fully set-up it you have sensors that provides you with your daily and annual net income and if you have configured it accordingly, you also have sensors available for the Home Assistant Energy Dashboard. 139 | 140 | The final result can look like this: 141 | 142 | ![checkwatt main](/images/ha_main.png) 143 | 144 | ## Events 145 | The integration publish [events](https://www.home-assistant.io/integrations/event/) which can be be used to drive automations. 146 | 147 | Below is a sample of an automation that acts when CheckWatt fails to engage your battery and deactivates it. 148 | 149 | ```yaml 150 | alias: Deactivated 151 | description: "" 152 | trigger: 153 | - platform: state 154 | entity_id: 155 | - event.skarfva_fcr_d_state 156 | attribute: event_type 157 | from: fcrd_activated 158 | to: fcrd_deactivated 159 | condition: [] 160 | action: 161 | - service: notify.faanskit 162 | data: 163 | message: "CheckWatt failed, please investigate." 164 | mode: single 165 | ``` 166 | 167 | 168 | # Expert Section 169 | If you think that some of the attributes provided should be sensors, please consider to use [Templates](https://www.home-assistant.io/docs/configuration/templating/) before you register it as an [issue](https://github.com/faanskit/ha-checkwatt/issues). If it can be done via a Template Sensor, it will most likely be rejected. 170 | 171 | ## Use templates 172 | This is an example of a Template based Sensor that pulls tomorrows estimated daily net income from the attribute of the CheckWatt Daily Net Income sensor. 173 | 174 | It goes without saying, but this should be put in your `configuration.yaml`: 175 | ```yaml 176 | template: 177 | - sensor: 178 | - name: "CheckWatt Tomorrow Net Income" 179 | unique_id: checkwatt_tomorrow_yield 180 | state: "{{ state_attr('sensor.skarfva_checkwatt_daily_yield', 'tomorrow_gross_income')}}" 181 | unit_of_measurement: "SEK" 182 | device_class: "monetary" 183 | state_class: total 184 | ``` 185 | The result will look something like this: 186 | 187 | ![template based sensor](/images/expert_sensor.png) 188 | 189 | 190 | ## Use developer tools 191 | The names of the attributes can be found in the Home Assistant Developer Tools section in your Home Assistant environment under the **STATES** sheet: 192 | 193 | ![home assistant developer tools](/images/dev_tools_states.png) 194 | 195 | # Acknowledgements 196 | This integration was loosely based on the [ha-esolar](https://github.com/faanskit/ha-esolar) integration. 197 | It was developed by [@faanskit](https://github.com/faanskit) with support from: 198 | 199 | - [@flopp999](https://github.com/flopp999) 200 | - [@angoyd](https://github.com/angoyd) 201 | 202 | This integration could not have been made without the excellent work done by the Home Assistant team. 203 | 204 | If you like what have been done here and want to help I would recommend that you firstly look into supporting Home Assistant. 205 | 206 | You can do this by purchasing some swag from their [store](https://home-assistant-store.creator-spring.com/) or paying for a Nabu Casa subscription. None of this could happen without them. 207 | 208 | # Licenses 209 | The integration is provided as-is without any warranties and published under [The MIT License](https://opensource.org/license/mit/). 210 | -------------------------------------------------------------------------------- /custom_components/checkwatt/__init__.py: -------------------------------------------------------------------------------- 1 | """The CheckWatt integration.""" 2 | 3 | from __future__ import annotations 4 | 5 | from datetime import time, timedelta 6 | import logging 7 | import random 8 | from typing import TypedDict 9 | 10 | from pycheckwatt import CheckwattManager, CheckWattRankManager 11 | import voluptuous as vol 12 | 13 | from homeassistant.config_entries import ConfigEntry 14 | from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform 15 | from homeassistant.core import ( 16 | HomeAssistant, 17 | ServiceCall, 18 | ServiceResponse, 19 | SupportsResponse, 20 | ) 21 | from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError 22 | from homeassistant.helpers import config_validation as cv 23 | from homeassistant.helpers.dispatcher import async_dispatcher_send 24 | from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed 25 | from homeassistant.util import dt as dt_util 26 | 27 | from .const import ( 28 | BASIC_TEST, 29 | CONF_CM10_SENSOR, 30 | CONF_CWR_NAME, 31 | CONF_POWER_SENSORS, 32 | CONF_PUSH_CW_TO_RANK, 33 | CONF_UPDATE_INTERVAL_ALL, 34 | CONF_UPDATE_INTERVAL_MONETARY, 35 | DOMAIN, 36 | EVENT_SIGNAL_FCRD, 37 | INTEGRATION_NAME, 38 | ) 39 | 40 | _LOGGER = logging.getLogger(__name__) 41 | 42 | PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.EVENT] 43 | 44 | UPDATE_HISTORY_SERVICE_NAME = "update_history" 45 | UPDATE_HISTORY_SCHEMA = vol.Schema( 46 | { 47 | vol.Required("start_date"): cv.date, 48 | vol.Required("end_date"): cv.date, 49 | } 50 | ) 51 | 52 | PUSH_CWR_SERVICE_NAME = "push_checkwatt_rank" 53 | PUSH_CWR_SCHEMA = None 54 | 55 | 56 | CHECKWATTRANK_REPORTER = "HomeAssistantV2" 57 | 58 | 59 | class CheckwattResp(TypedDict): 60 | """API response.""" 61 | 62 | id: str 63 | firstname: str 64 | lastname: str 65 | address: str 66 | zip: str 67 | city: str 68 | display_name: str 69 | dso: str 70 | energy_provider: str 71 | 72 | battery_power: float 73 | grid_power: float 74 | solar_power: float 75 | battery_soc: float 76 | charge_peak_ac: float 77 | charge_peak_dc: float 78 | discharge_peak_ac: float 79 | discharge_peak_dc: float 80 | monthly_grid_peak_power: float 81 | 82 | today_net_revenue: float 83 | tomorrow_net_revenue: float 84 | monthly_net_revenue: float 85 | annual_net_revenue: float 86 | month_estimate: float 87 | daily_average: float 88 | 89 | update_time: str 90 | next_update_time: str 91 | 92 | total_solar_energy: float 93 | total_charging_energy: float 94 | total_discharging_energy: float 95 | total_import_energy: float 96 | total_export_energy: float 97 | spot_price: float 98 | price_zone: str 99 | 100 | cm10_status: str 101 | cm10_version: str 102 | fcr_d_status: str 103 | fcr_d_info: str 104 | fcr_d_date: str 105 | reseller_id: int 106 | 107 | 108 | async def update_listener(hass: HomeAssistant, entry): 109 | """Handle options update.""" 110 | _LOGGER.debug(entry.options) 111 | if not hass: # Not sure, to remove warning 112 | await hass.config_entries.async_reload(entry.entry_id) 113 | 114 | 115 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 116 | """Set up CheckWatt from a config entry.""" 117 | coordinator = CheckwattCoordinator(hass, entry) 118 | await coordinator.async_config_entry_first_refresh() 119 | 120 | hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator 121 | entry.async_on_unload(entry.add_update_listener(update_listener)) 122 | await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) 123 | 124 | async def update_history_items(call: ServiceCall) -> ServiceResponse: 125 | """Fetch historical data from EIB and Update CheckWattRank.""" 126 | start_date = call.data["start_date"] 127 | end_date = call.data["end_date"] 128 | start_date_str = start_date.strftime("%Y-%m-%d") 129 | end_date_str = end_date.strftime("%Y-%m-%d") 130 | _LOGGER.debug( 131 | "Calling update_history service with start date: %s and end date %s", 132 | start_date_str, 133 | end_date_str, 134 | ) 135 | username = entry.data.get(CONF_USERNAME) 136 | password = entry.data.get(CONF_PASSWORD) 137 | cwr_name = entry.options.get(CONF_CWR_NAME) 138 | status = None 139 | stored_items = 0 140 | total_items = 0 141 | async with CheckwattManager(username, password, INTEGRATION_NAME) as cw: 142 | try: 143 | # Login to EnergyInBalance 144 | if await cw.login(): 145 | # Fetch customer detail 146 | if not await cw.get_customer_details(): 147 | _LOGGER.error("Failed to fetch customer details") 148 | return { 149 | "status": "Failed to fetch customer details", 150 | } 151 | 152 | if not await cw.get_price_zone(): 153 | _LOGGER.error("Failed to fetch prize zone") 154 | return { 155 | "status": "Failed to fetch prize zone", 156 | } 157 | 158 | hd = await cw.fetch_and_return_net_revenue( 159 | start_date_str, end_date_str 160 | ) 161 | if hd is None: 162 | _LOGGER.error("Failed to fetch revenue") 163 | return { 164 | "status": "Failed to fetch revenue", 165 | } 166 | 167 | energy_provider = await cw.get_energy_trading_company( 168 | cw.energy_provider_id 169 | ) 170 | 171 | async with CheckWattRankManager() as cwr: 172 | dso = "" 173 | if cw.battery_registration is not None: 174 | if "Dso" in cw.battery_registration: 175 | dso = cw.battery_registration["Dso"] 176 | 177 | ( 178 | status, 179 | stored_items, 180 | total_items, 181 | ) = await cwr.push_history_to_checkwatt_rank( 182 | display_name=( 183 | cwr_name if cwr_name != "" else cw.display_name 184 | ), 185 | dso=dso, 186 | electricity_company=energy_provider, 187 | electricity_area=cw.price_zone, 188 | installed_power=min( 189 | cw.battery_charge_peak_ac, 190 | cw.battery_discharge_peak_ac, 191 | ), 192 | reseller_id=cw.reseller_id, 193 | reporter=CHECKWATTRANK_REPORTER, 194 | historical_data=hd, 195 | ) 196 | 197 | else: 198 | status = "Failed to login." 199 | 200 | except InvalidAuth as err: 201 | raise ConfigEntryAuthFailed from err 202 | 203 | except CheckwattError as err: 204 | status = f"Failed to update CheckWattRank: {err}" 205 | 206 | return { 207 | "start_date": start_date_str, 208 | "end_date": end_date_str, 209 | "status": status, 210 | "stored_items": stored_items, 211 | "total_items": total_items, 212 | } 213 | 214 | async def push_cwr(call: ServiceCall) -> ServiceResponse: 215 | """Push data to CheckWattRank.""" 216 | username = entry.data.get(CONF_USERNAME) 217 | password = entry.data.get(CONF_PASSWORD) 218 | cwr_name = entry.options.get(CONF_CWR_NAME) 219 | status = None 220 | async with CheckwattManager(username, password, INTEGRATION_NAME) as cw: 221 | try: 222 | # Login to EnergyInBalance 223 | if await cw.login(): 224 | # Fetch customer detail 225 | if not await cw.get_customer_details(): 226 | _LOGGER.error("Failed to fetch customer details") 227 | return { 228 | "status": "Failed to fetch customer details", 229 | } 230 | 231 | if not await cw.get_price_zone(): 232 | _LOGGER.error("Failed to fetch prize zone") 233 | return { 234 | "status": "Failed to fetch prize zone", 235 | } 236 | 237 | if not await cw.get_fcrd_today_net_revenue(): 238 | raise UpdateFailed("Unknown error get_fcrd_revenue") 239 | 240 | display_name = cwr_name if cwr_name != "" else cw.display_name 241 | if await push_to_checkwatt_rank( 242 | cw, display_name, cw.fcrd_today_net_revenue 243 | ): 244 | status = "Data successfully sent to CheckWattRank" 245 | else: 246 | status = "Failed to update to CheckWattRank" 247 | 248 | else: 249 | status = "Failed to login." 250 | 251 | except InvalidAuth as err: 252 | raise ConfigEntryAuthFailed from err 253 | 254 | except CheckwattError as err: 255 | status = f"Failed to update CheckWattRank: {err}" 256 | 257 | return { 258 | "result": status, 259 | } 260 | 261 | hass.services.async_register( 262 | DOMAIN, 263 | UPDATE_HISTORY_SERVICE_NAME, 264 | update_history_items, 265 | schema=UPDATE_HISTORY_SCHEMA, 266 | supports_response=SupportsResponse.ONLY, 267 | ) 268 | 269 | hass.services.async_register( 270 | DOMAIN, 271 | PUSH_CWR_SERVICE_NAME, 272 | push_cwr, 273 | schema=PUSH_CWR_SCHEMA, 274 | supports_response=SupportsResponse.ONLY, 275 | ) 276 | 277 | return True 278 | 279 | 280 | async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 281 | """Unload a config entry.""" 282 | if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): 283 | hass.data[DOMAIN].pop(entry.entry_id) 284 | 285 | return unload_ok 286 | 287 | 288 | async def push_to_checkwatt_rank(cw_inst, cwr_name, today_net_income): 289 | """Push data to CheckWattRank.""" 290 | if cw_inst.fcrd_today_net_revenue is not None: 291 | energy_provider = await cw_inst.get_energy_trading_company( 292 | cw_inst.energy_provider_id 293 | ) 294 | async with CheckWattRankManager() as cwr: 295 | dso = "" 296 | if cw_inst.battery_registration is not None: 297 | if "Dso" in cw_inst.battery_registration: 298 | dso = cw_inst.battery_registration["Dso"] 299 | if await cwr.push_to_checkwatt_rank( 300 | display_name=(cwr_name if cwr_name != "" else cw_inst.display_name), 301 | dso=dso, 302 | electricity_company=energy_provider, 303 | electricity_area=cw_inst.price_zone, 304 | installed_power=min( 305 | cw_inst.battery_charge_peak_ac, cw_inst.battery_discharge_peak_ac 306 | ), 307 | today_net_income=today_net_income, 308 | reseller_id=cw_inst.reseller_id, 309 | reporter=CHECKWATTRANK_REPORTER, 310 | ): 311 | return True 312 | return False 313 | 314 | 315 | class CheckwattCoordinator(DataUpdateCoordinator[CheckwattResp]): 316 | """Data update coordinator.""" 317 | 318 | def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: 319 | """Initialize the coordinator.""" 320 | super().__init__( 321 | hass, 322 | _LOGGER, 323 | name=DOMAIN, 324 | update_interval=timedelta(minutes=CONF_UPDATE_INTERVAL_ALL), 325 | ) 326 | self._entry = entry 327 | self.last_cw_rank_push = None 328 | self.is_boot = True 329 | self.energy_provider = None 330 | self.fcrd_state = None 331 | self.fcrd_info = None 332 | self.fcrd_timestamp = None 333 | self._id = None 334 | self.update_all = 0 335 | self.random_offset = random.randint(0, 14) 336 | self.fcrd_today_net_revenue = None 337 | self.fcrd_tomorrow_net_revenue = None 338 | self.fcrd_month_net_revenue = None 339 | self.fcrd_month_net_estimate = None 340 | self.fcrd_daily_net_average = None 341 | self.fcrd_year_net_revenue = None 342 | self.monthly_grid_peak_power = None 343 | 344 | @property 345 | def entry_id(self) -> str: 346 | """Return entry ID.""" 347 | return self._entry.entry_id 348 | 349 | async def _async_update_data(self) -> CheckwattResp: # noqa: C901 350 | """Fetch the latest data from the source.""" 351 | 352 | try: 353 | username = self._entry.data.get(CONF_USERNAME) 354 | password = self._entry.data.get(CONF_PASSWORD) 355 | use_power_sensors = self._entry.options.get(CONF_POWER_SENSORS) 356 | push_to_cw_rank = self._entry.options.get(CONF_PUSH_CW_TO_RANK) 357 | use_cm10_sensor = self._entry.options.get(CONF_CM10_SENSOR) 358 | cwr_name = self._entry.options.get(CONF_CWR_NAME) 359 | 360 | async with CheckwattManager( 361 | username, password, INTEGRATION_NAME 362 | ) as cw_inst: 363 | if not await cw_inst.login(): 364 | _LOGGER.error("Failed to login, abort update") 365 | raise UpdateFailed("Failed to login") 366 | 367 | if not await cw_inst.get_customer_details(): 368 | _LOGGER.error("Failed to obtain customer details, abort update") 369 | raise UpdateFailed("Unknown error get_customer_details") 370 | 371 | if not await cw_inst.get_energy_flow(): 372 | _LOGGER.error("Failed to get energy flows, abort update") 373 | raise UpdateFailed("Unknown error get_energy_flow") 374 | 375 | if use_cm10_sensor: 376 | if not await cw_inst.get_meter_status(): 377 | _LOGGER.error("Failed to obtain meter details, abort update") 378 | raise UpdateFailed("Unknown error get_meter_status") 379 | 380 | # Only fetch some parameters every 15 min 381 | if self.update_all == 0 and not self.is_boot: 382 | self.update_all = CONF_UPDATE_INTERVAL_MONETARY 383 | _LOGGER.debug("Fetching daily revenue") 384 | if not await cw_inst.get_fcrd_today_net_revenue(): 385 | raise UpdateFailed("Unknown error get_fcrd_revenue") 386 | 387 | _LOGGER.debug("Fetching monthly revenue") 388 | if not await cw_inst.get_fcrd_month_net_revenue(): 389 | raise UpdateFailed("Unknown error get_revenue_month") 390 | 391 | _LOGGER.debug("Fetching annual revenue") 392 | if not await cw_inst.get_fcrd_year_net_revenue(): 393 | raise UpdateFailed("Unknown error get_revenue_year") 394 | 395 | _LOGGER.debug("Fetching montly peak power") 396 | if not await cw_inst.get_battery_month_peak_effect(): 397 | raise UpdateFailed( 398 | "Unknown error get_battery_month_peak_effect" 399 | ) 400 | 401 | self.fcrd_today_net_revenue = cw_inst.fcrd_today_net_revenue 402 | self.fcrd_tomorrow_net_revenue = cw_inst.fcrd_tomorrow_net_revenue 403 | self.fcrd_month_net_revenue = cw_inst.fcrd_month_net_revenue 404 | self.fcrd_month_net_estimate = cw_inst.fcrd_month_net_estimate 405 | self.fcrd_daily_net_average = cw_inst.fcrd_daily_net_average 406 | self.fcrd_year_net_revenue = cw_inst.fcrd_year_net_revenue 407 | self.monthly_grid_peak_power = cw_inst.month_peak_effect 408 | 409 | if not self.is_boot: 410 | self.update_all -= 1 411 | 412 | if self.is_boot: 413 | self.is_boot = False 414 | self.energy_provider = await cw_inst.get_energy_trading_company( 415 | cw_inst.energy_provider_id 416 | ) 417 | 418 | # Store fcrd_state at boot, used to spark event 419 | self.fcrd_state = cw_inst.fcrd_state 420 | self._id = cw_inst.customer_details["Id"] 421 | 422 | # Price Zone is used both as Detailed Sensor and by Push to CheckWattRank 423 | if push_to_cw_rank or use_power_sensors: 424 | if not await cw_inst.get_price_zone(): 425 | raise UpdateFailed("Unknown error get_price_zone") 426 | if use_power_sensors: 427 | if not await cw_inst.get_power_data(): 428 | raise UpdateFailed("Unknown error get_power_data") 429 | if not await cw_inst.get_spot_price(): 430 | raise UpdateFailed("Unknown error get_spot_price") 431 | 432 | if push_to_cw_rank: 433 | if self.last_cw_rank_push is None or ( 434 | dt_util.now().time() 435 | >= time(9, self.random_offset) # Wait until 9am +- 15 min 436 | and dt_util.start_of_local_day(dt_util.now()) 437 | != dt_util.start_of_local_day(self.last_cw_rank_push) 438 | ): 439 | if self.fcrd_today_net_revenue is not None: 440 | _LOGGER.debug("Pushing to CheckWattRank") 441 | if await push_to_checkwatt_rank( 442 | cw_inst, cwr_name, self.fcrd_today_net_revenue 443 | ): 444 | self.last_cw_rank_push = dt_util.now() 445 | 446 | resp: CheckwattResp = { 447 | "id": cw_inst.customer_details["Id"], 448 | "firstname": cw_inst.customer_details["FirstName"], 449 | "lastname": cw_inst.customer_details["LastName"], 450 | "address": cw_inst.customer_details["StreetAddress"], 451 | "zip": cw_inst.customer_details["ZipCode"], 452 | "city": cw_inst.customer_details["City"], 453 | "display_name": cw_inst.display_name, 454 | "energy_provider": self.energy_provider, 455 | "reseller_id": cw_inst.reseller_id, 456 | } 457 | if cw_inst.battery_registration is not None: 458 | if "Dso" in cw_inst.battery_registration: 459 | resp["dso"] = cw_inst.battery_registration["Dso"] 460 | if cw_inst.energy_data is not None: 461 | resp["battery_power"] = cw_inst.battery_power 462 | resp["grid_power"] = cw_inst.grid_power 463 | resp["solar_power"] = cw_inst.solar_power 464 | resp["battery_soc"] = cw_inst.battery_soc 465 | resp["charge_peak_ac"] = cw_inst.battery_charge_peak_ac 466 | resp["charge_peak_dc"] = cw_inst.battery_charge_peak_dc 467 | resp["discharge_peak_ac"] = cw_inst.battery_discharge_peak_ac 468 | resp["discharge_peak_dc"] = cw_inst.battery_discharge_peak_dc 469 | resp["monthly_grid_peak_power"] = self.monthly_grid_peak_power 470 | 471 | # Use self stored variant of revenue parameters as they are not always fetched 472 | if self.fcrd_today_net_revenue is not None: 473 | resp["today_net_revenue"] = self.fcrd_today_net_revenue 474 | resp["tomorrow_net_revenue"] = self.fcrd_tomorrow_net_revenue 475 | if self.fcrd_month_net_revenue is not None: 476 | resp["monthly_net_revenue"] = self.fcrd_month_net_revenue 477 | resp["month_estimate"] = self.fcrd_month_net_estimate 478 | resp["daily_average"] = self.fcrd_daily_net_average 479 | if self.fcrd_year_net_revenue is not None: 480 | resp["annual_net_revenue"] = self.fcrd_year_net_revenue 481 | 482 | update_time = dt_util.now().strftime("%Y-%m-%d %H:%M:%S") 483 | next_update = dt_util.now() + timedelta( 484 | minutes=CONF_UPDATE_INTERVAL_MONETARY 485 | ) 486 | next_update_time = next_update.strftime("%Y-%m-%d %H:%M:%S") 487 | resp["update_time"] = update_time 488 | resp["next_update_time"] = next_update_time 489 | 490 | if use_power_sensors: 491 | resp["total_solar_energy"] = cw_inst.total_solar_energy 492 | resp["total_charging_energy"] = cw_inst.total_charging_energy 493 | resp["total_discharging_energy"] = cw_inst.total_discharging_energy 494 | resp["total_import_energy"] = cw_inst.total_import_energy 495 | resp["total_export_energy"] = cw_inst.total_export_energy 496 | resp["spot_price"] = cw_inst.get_spot_price_excl_vat( 497 | int(dt_util.now().strftime("%H")) 498 | ) 499 | resp["price_zone"] = cw_inst.price_zone 500 | 501 | if cw_inst.meter_data is not None and use_cm10_sensor: 502 | if cw_inst.meter_status == "offline": 503 | resp["cm10_status"] = "Offline" 504 | elif cw_inst.meter_under_test: 505 | resp["cm10_status"] = "Test Pending" 506 | else: 507 | resp["cm10_status"] = "Active" 508 | 509 | resp["cm10_version"] = cw_inst.meter_version 510 | resp["fcr_d_status"] = cw_inst.fcrd_state 511 | resp["fcr_d_info"] = cw_inst.fcrd_info 512 | resp["fcr_d_date"] = cw_inst.fcrd_timestamp 513 | 514 | # Check if FCR-D State has changed and dispatch it ACTIVATED/ DEACTIVATED 515 | old_state = self.fcrd_state 516 | new_state = cw_inst.fcrd_state 517 | 518 | # During test, toggle (every minute) 519 | if BASIC_TEST is True: 520 | if old_state == "ACTIVATED": 521 | new_state = "DEACTIVATE" 522 | if old_state == "DEACTIVATE": 523 | new_state = "FAIL ACTIVATION" 524 | if old_state == "FAIL ACTIVATION": 525 | new_state = "ACTIVATED" 526 | 527 | if old_state != new_state: 528 | signal_payload = { 529 | "signal": EVENT_SIGNAL_FCRD, 530 | "data": { 531 | "current_fcrd": { 532 | "state": old_state, 533 | "info": self.fcrd_info, 534 | "date": self.fcrd_timestamp, 535 | }, 536 | "new_fcrd": { 537 | "state": new_state, 538 | "info": cw_inst.fcrd_info, 539 | "date": cw_inst.fcrd_timestamp, 540 | }, 541 | }, 542 | } 543 | 544 | # Dispatch it to subscribers 545 | async_dispatcher_send( 546 | self.hass, 547 | f"checkwatt_{self._id}_signal", 548 | signal_payload, 549 | ) 550 | 551 | # Update self to discover next change 552 | self.fcrd_state = new_state 553 | self.fcrd_info = cw_inst.fcrd_info 554 | self.fcrd_timestamp = cw_inst.fcrd_timestamp 555 | 556 | return resp 557 | 558 | except InvalidAuth as err: 559 | raise ConfigEntryAuthFailed from err 560 | except CheckwattError as err: 561 | raise UpdateFailed(str(err)) from err 562 | 563 | 564 | class CheckwattError(HomeAssistantError): 565 | """Base error.""" 566 | 567 | 568 | class InvalidAuth(CheckwattError): 569 | """Raised when invalid authentication credentials are provided.""" 570 | 571 | 572 | class APIRatelimitExceeded(CheckwattError): 573 | """Raised when the API rate limit is exceeded.""" 574 | 575 | 576 | class UnknownError(CheckwattError): 577 | """Raised when an unknown error occurs.""" 578 | -------------------------------------------------------------------------------- /custom_components/checkwatt/config_flow.py: -------------------------------------------------------------------------------- 1 | """Config flow for CheckWatt integration.""" 2 | 3 | from __future__ import annotations 4 | 5 | import logging 6 | from typing import Any 7 | 8 | from pycheckwatt import CheckwattManager 9 | import voluptuous as vol 10 | 11 | from homeassistant import config_entries 12 | from homeassistant.const import CONF_PASSWORD, CONF_USERNAME 13 | from homeassistant.core import HomeAssistant, callback 14 | from homeassistant.data_entry_flow import FlowResult 15 | from homeassistant.exceptions import HomeAssistantError 16 | 17 | from .const import ( 18 | CONF_CM10_SENSOR, 19 | CONF_CWR_NAME, 20 | CONF_POWER_SENSORS, 21 | CONF_PUSH_CW_TO_RANK, 22 | DOMAIN, 23 | ) 24 | 25 | CONF_TITLE = "CheckWatt" 26 | 27 | _LOGGER = logging.getLogger(__name__) 28 | 29 | STEP_USER_DATA_SCHEMA = vol.Schema( 30 | { 31 | vol.Required(CONF_USERNAME): str, 32 | vol.Required(CONF_PASSWORD): str, 33 | } 34 | ) 35 | 36 | 37 | async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: 38 | """Validate that the user input allows us to connect to CheckWatt.""" 39 | async with CheckwattManager( 40 | data[CONF_USERNAME], data[CONF_PASSWORD] 41 | ) as check_watt_instance: 42 | if not await check_watt_instance.login(): 43 | raise InvalidAuth 44 | 45 | 46 | class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 47 | """Handle a config flow for CheckWatt.""" 48 | 49 | VERSION = 1 50 | 51 | def __init__(self) -> None: 52 | """Set up the the config flow.""" 53 | self.data = {} 54 | 55 | async def async_step_user( 56 | self, user_input: dict[str, Any] | None = None 57 | ) -> FlowResult: 58 | """Handle the initial step. Username and password.""" 59 | if user_input is None: 60 | return self.async_show_form( 61 | step_id="user", data_schema=STEP_USER_DATA_SCHEMA 62 | ) 63 | 64 | errors = {} 65 | self.data = user_input 66 | try: 67 | await validate_input(self.hass, self.data) 68 | except CannotConnect: 69 | errors["base"] = "cannot_connect" 70 | except InvalidAuth: 71 | errors["base"] = "invalid_auth" 72 | except Exception: # pylint: disable=broad-except 73 | _LOGGER.exception("Unexpected exception") 74 | errors["base"] = "unknown" 75 | else: 76 | return self.async_create_entry( 77 | title=CONF_TITLE, 78 | data=self.data, 79 | options={ 80 | CONF_POWER_SENSORS: False, 81 | CONF_PUSH_CW_TO_RANK: False, 82 | CONF_CM10_SENSOR: True, 83 | CONF_CWR_NAME: "", 84 | }, 85 | ) 86 | 87 | return self.async_show_form( 88 | step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors 89 | ) 90 | 91 | @staticmethod 92 | @callback 93 | def async_get_options_flow( 94 | config_entry: config_entries.ConfigEntry, 95 | ) -> config_entries.OptionsFlow: 96 | """Create the options flow.""" 97 | return OptionsFlowHandler(config_entry) 98 | 99 | 100 | class OptionsFlowHandler(config_entries.OptionsFlow): 101 | """Handle a options flow for CheckWatt.""" 102 | 103 | def __init__(self, config_entry: config_entries.ConfigEntry) -> None: 104 | """Initialize options flow.""" 105 | self.config_entry = config_entry 106 | 107 | async def async_step_init( 108 | self, user_input: dict[str, Any] | None = None 109 | ) -> FlowResult: 110 | """Manage the options.""" 111 | if user_input is not None: 112 | return self.async_create_entry(title=CONF_TITLE, data=user_input) 113 | 114 | return self.async_show_form( 115 | step_id="init", 116 | data_schema=vol.Schema( 117 | { 118 | vol.Required( 119 | CONF_POWER_SENSORS, 120 | default=self.config_entry.options.get(CONF_POWER_SENSORS), 121 | ): bool, 122 | vol.Required( 123 | CONF_PUSH_CW_TO_RANK, 124 | default=self.config_entry.options.get(CONF_PUSH_CW_TO_RANK), 125 | ): bool, 126 | vol.Required( 127 | CONF_CM10_SENSOR, 128 | default=self.config_entry.options.get(CONF_CM10_SENSOR), 129 | ): bool, 130 | vol.Optional( 131 | CONF_CWR_NAME, 132 | default=self.config_entry.options.get(CONF_CWR_NAME), 133 | ): str, 134 | } 135 | ), 136 | ) 137 | 138 | 139 | class CannotConnect(HomeAssistantError): 140 | """Error to indicate we cannot connect.""" 141 | 142 | 143 | class InvalidAuth(HomeAssistantError): 144 | """Error to indicate there is invalid auth.""" 145 | -------------------------------------------------------------------------------- /custom_components/checkwatt/const.py: -------------------------------------------------------------------------------- 1 | """Constants for the CheckWatt integration.""" 2 | 3 | from typing import Final 4 | 5 | DOMAIN = "checkwatt" 6 | INTEGRATION_NAME = "ha-checkwatt" 7 | 8 | # Update interval for regular sensors is once every minute 9 | CONF_UPDATE_INTERVAL_ALL = 1 10 | CONF_UPDATE_INTERVAL_MONETARY = 15 11 | ATTRIBUTION = "Data provided by CheckWatt EnergyInBalance" 12 | MANUFACTURER = "CheckWatt" 13 | CHECKWATT_MODEL = "CheckWatt" 14 | 15 | CONF_POWER_SENSORS: Final = "show_details" 16 | CONF_PUSH_CW_TO_RANK: Final = "push_to_cw_rank" 17 | CONF_CM10_SENSOR: Final = "cm10_sensor" 18 | CONF_CWR_NAME: Final = "cwr_name" 19 | 20 | # Misc 21 | P_UNKNOWN = "Unknown" 22 | 23 | # Temp Test 24 | BASIC_TEST = False 25 | 26 | # CheckWatt Sensor Attributes 27 | # NOTE Keep these names aligned with strings.json 28 | # 29 | C_ADR = "street_address" 30 | C_BATTERY_POWER = "battery_power" 31 | C_CITY = "city" 32 | C_CM10_VERSION = "cm10_version" 33 | C_DAILY_AVERAGE = "daily_average" 34 | C_DISPLAY_NAME = "display_name" 35 | C_DSO = "dso" 36 | C_GRID_POWER = "grid_power" 37 | C_ENERGY_PROVIDER = "energy_provider" 38 | C_FCRD_DATE = "fcr_d_date" 39 | C_FCRD_INFO = "fcr_d_info" 40 | C_FCRD_STATUS = "fcr_d_status" 41 | C_MONTH_ESITIMATE = "month_estimate" 42 | C_NEXT_UPDATE_TIME = "next_update" 43 | C_PRICE_ZONE = "price_zone" 44 | C_RESELLER_ID = "reseller_id" 45 | C_SOLAR_POWER = "solar_power" 46 | C_UPDATE_TIME = "last_update" 47 | C_TOMORROW_REVENUE = "tomorrow_net_revenue" 48 | C_VAT = "vat" 49 | C_ZIP = "zip_code" 50 | C_CHARGE_PEAK_AC = "charge_peak_ac" 51 | C_CHARGE_PEAK_DC = "charge_peak_dc" 52 | C_DISCHARGE_PEAK_AC = "discharge_peak_ac" 53 | C_DISCHARGE_PEAK_DC = "discharge_peak_dc" 54 | C_MONTHLY_GRID_PEAK_POWER = "monthly_grid_peak_power" 55 | 56 | # CheckWatt Event Signals 57 | EVENT_SIGNAL_FCRD = "fcrd" 58 | -------------------------------------------------------------------------------- /custom_components/checkwatt/event.py: -------------------------------------------------------------------------------- 1 | """Events for CheckWatt.""" 2 | 3 | from __future__ import annotations 4 | 5 | import logging 6 | 7 | from homeassistant.components.event import EventEntity, EventEntityDescription 8 | from homeassistant.config_entries import ConfigEntry 9 | from homeassistant.core import HomeAssistant, callback 10 | from homeassistant.helpers.device_registry import DeviceInfo 11 | from homeassistant.helpers.dispatcher import async_dispatcher_connect 12 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 13 | from homeassistant.helpers.update_coordinator import CoordinatorEntity 14 | 15 | from . import CheckwattCoordinator, CheckwattResp 16 | from .const import ATTRIBUTION, CHECKWATT_MODEL, DOMAIN, EVENT_SIGNAL_FCRD, MANUFACTURER 17 | 18 | EVENT_FCRD_ACTIVATED = "fcrd_activated" 19 | EVENT_FCRD_DEACTIVATED = "fcrd_deactivated" 20 | EVENT_FCRD_FAILED = "fcrd_failed" 21 | 22 | _LOGGER = logging.getLogger(__name__) 23 | 24 | 25 | async def async_setup_entry( 26 | hass: HomeAssistant, 27 | entry: ConfigEntry, 28 | async_add_entities: AddEntitiesCallback, 29 | ) -> None: 30 | """Set up the CheckWatt event platform.""" 31 | coordinator: CheckwattCoordinator = hass.data[DOMAIN][entry.entry_id] 32 | entities: list[AbstractCheckwattEvent] = [] 33 | checkwatt_data: CheckwattResp = coordinator.data 34 | _LOGGER.debug( 35 | "Setting up detailed CheckWatt event for %s", 36 | checkwatt_data["display_name"], 37 | ) 38 | 39 | event_description = EventEntityDescription( 40 | key="fcr_d_event", 41 | name="FCR-D State", 42 | icon="mdi:battery-alert", 43 | device_class="fcrd", 44 | translation_key="fcr_d_event", 45 | ) 46 | 47 | entities.append(CheckWattFCRDEvent(coordinator, event_description)) 48 | async_add_entities(entities, True) 49 | 50 | 51 | class AbstractCheckwattEvent(CoordinatorEntity[CheckwattCoordinator], EventEntity): 52 | """Abstract class for an CheckWatt event.""" 53 | 54 | _attr_attribution = ATTRIBUTION 55 | _attr_has_entity_name = True 56 | 57 | def __init__( 58 | self, 59 | coordinator: CheckwattCoordinator, 60 | description: EventEntity, 61 | ) -> None: 62 | """Initialize the event.""" 63 | super().__init__(coordinator) 64 | self._coordinator = coordinator 65 | self._device_model = CHECKWATT_MODEL 66 | self._device_name = coordinator.data["display_name"] 67 | self._id = coordinator.data["id"] 68 | self.entity_description = description 69 | self._attr_unique_id = ( 70 | f'checkwattUid_{description.key}_{coordinator.data["id"]}' 71 | ) 72 | 73 | @property 74 | def device_info(self) -> DeviceInfo: 75 | """Return the device_info of the device.""" 76 | device_info = DeviceInfo( 77 | identifiers={(DOMAIN, self._id)}, 78 | manufacturer=MANUFACTURER, 79 | model=self._device_model, 80 | name=self._device_name, 81 | ) 82 | return device_info 83 | 84 | 85 | class CheckWattFCRDEvent(AbstractCheckwattEvent): 86 | """Representation of a CheckWatt sleep event.""" 87 | 88 | _attr_event_types = [ 89 | EVENT_FCRD_ACTIVATED, 90 | EVENT_FCRD_DEACTIVATED, 91 | EVENT_FCRD_FAILED, 92 | ] 93 | 94 | def __init__( 95 | self, 96 | coordinator: CheckwattCoordinator, 97 | description: EventEntityDescription, 98 | ) -> None: 99 | """Initialize the CheckWatt event entity.""" 100 | super().__init__(coordinator=coordinator, description=description) 101 | self._coordinator = coordinator 102 | 103 | async def async_added_to_hass(self) -> None: 104 | """Register callbacks.""" 105 | super().async_added_to_hass() 106 | self.async_on_remove( 107 | async_dispatcher_connect( 108 | self.hass, 109 | f"checkwatt_{self._id}_signal", 110 | self.handle_event, 111 | ), 112 | ) 113 | 114 | # Send the status upon boot 115 | if "fcr_d_status" in self._coordinator.data: 116 | event = None 117 | if self._coordinator.data["fcr_d_status"] == "ACTIVATED": 118 | event = EVENT_FCRD_ACTIVATED 119 | elif self._coordinator.data["fcr_d_status"] == "DEACTIVATE": 120 | event = EVENT_FCRD_DEACTIVATED 121 | elif self._coordinator.data["fcr_d_status"] == "FAIL ACTIVATION": 122 | event = EVENT_FCRD_FAILED 123 | 124 | if event is not None: 125 | self._trigger_event(event) 126 | self.async_write_ha_state() 127 | 128 | @callback 129 | def handle_event(self, signal_payload) -> None: 130 | """Handle received event.""" 131 | event = None 132 | if "signal" in signal_payload and signal_payload["signal"] == EVENT_SIGNAL_FCRD: 133 | if ( 134 | "new_fcrd" in signal_payload["data"] 135 | and "state" in signal_payload["data"]["new_fcrd"] 136 | ): 137 | if signal_payload["data"]["new_fcrd"]["state"] == "ACTIVATED": 138 | event = EVENT_FCRD_ACTIVATED 139 | elif signal_payload["data"]["new_fcrd"]["state"] == "DEACTIVATE": 140 | event = EVENT_FCRD_DEACTIVATED 141 | elif signal_payload["data"]["new_fcrd"]["state"] == "FAIL ACTIVATION": 142 | event = EVENT_FCRD_FAILED 143 | 144 | else: 145 | _LOGGER.error( 146 | "Signal %s payload did not include correct data", EVENT_SIGNAL_FCRD 147 | ) 148 | 149 | # Add additional signals and events here 150 | # Eg: 151 | # elif signal_payload["signal"] == EVENT_SIGNAL_NEW 152 | 153 | else: 154 | _LOGGER.error("Signal did not include a known signal") 155 | 156 | if event is not None: 157 | self._trigger_event(event) 158 | self.async_write_ha_state() 159 | -------------------------------------------------------------------------------- /custom_components/checkwatt/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "checkwatt", 3 | "name": "CheckWatt", 4 | "codeowners": [ 5 | "@faanskit", 6 | "@angoyd", 7 | "@flopp999" 8 | ], 9 | "config_flow": true, 10 | "dependencies": [], 11 | "documentation": "https://github.com/faanskit/ha-checkwatt#readme", 12 | "homekit": {}, 13 | "iot_class": "cloud_polling", 14 | "issue_tracker": "https://github.com/faanskit/ha-checkwatt/issues", 15 | "requirements": ["pycheckwatt>=0.2.8", "aiohttp>=3.9.1"], 16 | "ssdp": [], 17 | "version": "0.2.3", 18 | "zeroconf": [] 19 | } 20 | 21 | -------------------------------------------------------------------------------- /custom_components/checkwatt/sensor.py: -------------------------------------------------------------------------------- 1 | """Support for CheckWatt sensors.""" 2 | 3 | from __future__ import annotations 4 | 5 | from datetime import timedelta 6 | import logging 7 | 8 | from homeassistant.components.sensor import ( 9 | SensorDeviceClass, 10 | SensorEntity, 11 | SensorEntityDescription, 12 | SensorStateClass, 13 | ) 14 | from homeassistant.config_entries import ConfigEntry 15 | from homeassistant.const import PERCENTAGE, UnitOfEnergy 16 | from homeassistant.core import HomeAssistant, callback 17 | from homeassistant.helpers.entity import DeviceInfo 18 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 19 | from homeassistant.helpers.update_coordinator import CoordinatorEntity 20 | 21 | from . import CheckwattCoordinator, CheckwattResp 22 | from .const import ( 23 | ATTRIBUTION, 24 | C_ADR, 25 | C_BATTERY_POWER, 26 | C_CHARGE_PEAK_AC, 27 | C_CHARGE_PEAK_DC, 28 | C_CITY, 29 | C_CM10_VERSION, 30 | C_DAILY_AVERAGE, 31 | C_DISCHARGE_PEAK_AC, 32 | C_DISCHARGE_PEAK_DC, 33 | C_DISPLAY_NAME, 34 | C_DSO, 35 | C_ENERGY_PROVIDER, 36 | C_FCRD_DATE, 37 | C_FCRD_INFO, 38 | C_FCRD_STATUS, 39 | C_GRID_POWER, 40 | C_MONTH_ESITIMATE, 41 | C_MONTHLY_GRID_PEAK_POWER, 42 | C_NEXT_UPDATE_TIME, 43 | C_PRICE_ZONE, 44 | C_RESELLER_ID, 45 | C_SOLAR_POWER, 46 | C_TOMORROW_REVENUE, 47 | C_UPDATE_TIME, 48 | C_VAT, 49 | C_ZIP, 50 | CHECKWATT_MODEL, 51 | CONF_CM10_SENSOR, 52 | CONF_POWER_SENSORS, 53 | DOMAIN, 54 | MANUFACTURER, 55 | ) 56 | 57 | DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) 58 | 59 | _LOGGER = logging.getLogger(__name__) 60 | 61 | CHECKWATT_MONETARY_SENSORS: dict[str, SensorEntityDescription] = { 62 | "daily": SensorEntityDescription( 63 | key="daily_yield", 64 | name="CheckWatt Daily Yield", 65 | icon="mdi:account-cash", 66 | device_class=SensorDeviceClass.MONETARY, 67 | native_unit_of_measurement="SEK", 68 | state_class=SensorStateClass.TOTAL, 69 | translation_key="daily_yield_sensor", 70 | ), 71 | "monthly": SensorEntityDescription( 72 | key="monthly_yield", 73 | name="CheckWatt Monthly Yield", 74 | icon="mdi:account-cash-outline", 75 | device_class=SensorDeviceClass.MONETARY, 76 | native_unit_of_measurement="SEK", 77 | state_class=SensorStateClass.TOTAL, 78 | translation_key="monthly_yield_sensor", 79 | ), 80 | "annual": SensorEntityDescription( 81 | key="annual_yield", 82 | name="CheckWatt Annual Yield", 83 | icon="mdi:account-cash-outline", 84 | device_class=SensorDeviceClass.MONETARY, 85 | native_unit_of_measurement="SEK", 86 | state_class=SensorStateClass.TOTAL, 87 | translation_key="annual_yield_sensor", 88 | ), 89 | "battery": SensorEntityDescription( 90 | key="battery_soc", 91 | name="CheckWatt Battery SoC", 92 | device_class=SensorDeviceClass.BATTERY, 93 | native_unit_of_measurement=PERCENTAGE, 94 | state_class=SensorStateClass.MEASUREMENT, 95 | translation_key="battery_soc_sensor", 96 | ), 97 | "cm10": SensorEntityDescription( 98 | key="cm10", 99 | name="CheckWatt CM10 Status", 100 | icon="mdi:raspberry-pi", 101 | translation_key="cm10_sensor", 102 | ), 103 | } 104 | 105 | 106 | CHECKWATT_ENERGY_SENSORS: dict[str, SensorEntityDescription] = { 107 | "total_solar_energy": SensorEntityDescription( 108 | key="solar", 109 | name="Solar Energy", 110 | icon="mdi:solar-power-variant-outline", 111 | device_class=SensorDeviceClass.ENERGY, 112 | native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, 113 | state_class=SensorStateClass.TOTAL_INCREASING, 114 | translation_key="solar_sensor", 115 | ), 116 | "total_charging_energy": SensorEntityDescription( 117 | key="charging", 118 | name="Battery Charging Energy", 119 | icon="mdi:home-battery", 120 | device_class=SensorDeviceClass.ENERGY, 121 | native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, 122 | state_class=SensorStateClass.TOTAL_INCREASING, 123 | translation_key="charging_sensor", 124 | ), 125 | "total_discharging_energy": SensorEntityDescription( 126 | key="discharging", 127 | name="Battery Discharging Energy", 128 | icon="mdi:home-battery-outline", 129 | device_class=SensorDeviceClass.ENERGY, 130 | native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, 131 | state_class=SensorStateClass.TOTAL_INCREASING, 132 | translation_key="discharging_sensor", 133 | ), 134 | "total_import_energy": SensorEntityDescription( 135 | key="import", 136 | name="Import Energy", 137 | icon="mdi:transmission-tower-export", 138 | device_class=SensorDeviceClass.ENERGY, 139 | native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, 140 | state_class=SensorStateClass.TOTAL_INCREASING, 141 | translation_key="import_sensor", 142 | ), 143 | "total_export_energy": SensorEntityDescription( 144 | key="export", 145 | name="Export Energy", 146 | icon="mdi:transmission-tower-import", 147 | device_class=SensorDeviceClass.ENERGY, 148 | native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, 149 | state_class=SensorStateClass.TOTAL_INCREASING, 150 | translation_key="export_sensor", 151 | ), 152 | } 153 | 154 | 155 | CHECKWATT_SPOTPRICE_SENSORS: dict[str, SensorEntityDescription] = { 156 | "excl_vat": SensorEntityDescription( 157 | key="spot_price", 158 | name="Spot Price", 159 | icon="mdi:chart-line", 160 | device_class=SensorDeviceClass.MONETARY, 161 | native_unit_of_measurement="SEK/kWh", 162 | state_class=SensorStateClass.TOTAL, 163 | translation_key="spot_price_sensor", 164 | ), 165 | "inc_vat": SensorEntityDescription( 166 | key="spot_price_vat", 167 | name="Spot Price incl. VAT", 168 | icon="mdi:chart-multiple", 169 | device_class=SensorDeviceClass.MONETARY, 170 | native_unit_of_measurement="SEK/kWh", 171 | state_class=SensorStateClass.TOTAL, 172 | translation_key="spot_price_vat_sensor", 173 | ), 174 | } 175 | 176 | 177 | async def async_setup_entry( 178 | hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback 179 | ) -> None: 180 | """Set up the CheckWatt sensor.""" 181 | coordinator: CheckwattCoordinator = hass.data[DOMAIN][entry.entry_id] 182 | entities: list[AbstractCheckwattSensor] = [] 183 | checkwatt_data: CheckwattResp = coordinator.data 184 | use_power_sensors = entry.options.get(CONF_POWER_SENSORS) 185 | use_cm10_sensor = entry.options.get(CONF_CM10_SENSOR) 186 | 187 | _LOGGER.debug("Setting up CheckWatt sensor for %s", checkwatt_data["display_name"]) 188 | for key, description in CHECKWATT_MONETARY_SENSORS.items(): 189 | if key == "daily": 190 | entities.append(CheckwattSensor(coordinator, description)) 191 | elif key == "monthly": 192 | entities.append(CheckwattMonthlySensor(coordinator, description)) 193 | elif key == "annual": 194 | entities.append(CheckwattAnnualSensor(coordinator, description)) 195 | elif key == "battery": 196 | entities.append(CheckwattBatterySoCSensor(coordinator, description)) 197 | elif key == "cm10" and use_cm10_sensor: 198 | entities.append(CheckwattCM10Sensor(coordinator, description)) 199 | 200 | if use_power_sensors: 201 | _LOGGER.debug( 202 | "Setting up detailed CheckWatt sensors for %s", 203 | checkwatt_data["display_name"], 204 | ) 205 | for data_key, description in CHECKWATT_ENERGY_SENSORS.items(): 206 | entities.append(CheckwattEnergySensor(coordinator, description, data_key)) 207 | for vat_key, description in CHECKWATT_SPOTPRICE_SENSORS.items(): 208 | entities.append(CheckwattSpotPriceSensor(coordinator, description, vat_key)) 209 | 210 | async_add_entities(entities, True) 211 | 212 | 213 | class AbstractCheckwattSensor(CoordinatorEntity[CheckwattCoordinator], SensorEntity): 214 | """Abstract class for an CheckWatt sensor.""" 215 | 216 | _attr_attribution = ATTRIBUTION 217 | _attr_has_entity_name = True 218 | 219 | def __init__( 220 | self, 221 | coordinator: CheckwattCoordinator, 222 | description: SensorEntityDescription, 223 | ) -> None: 224 | """Initialize the sensor.""" 225 | _LOGGER.debug("Creating %s sensor", description.name) 226 | super().__init__(coordinator) 227 | self._coordinator = coordinator 228 | self._device_model = CHECKWATT_MODEL 229 | self._device_name = coordinator.data["display_name"] 230 | self._id = coordinator.data["id"] 231 | self.entity_description = description 232 | self._attr_unique_id = ( 233 | f'checkwattUid_{description.key}_{coordinator.data["id"]}' 234 | ) 235 | self._attr_extra_state_attributes = {} 236 | 237 | @property 238 | def device_info(self) -> DeviceInfo: 239 | """Return the device_info of the device.""" 240 | device_info = DeviceInfo( 241 | identifiers={(DOMAIN, self._id)}, 242 | manufacturer=MANUFACTURER, 243 | model=self._device_model, 244 | name=self._device_name, 245 | ) 246 | return device_info 247 | 248 | 249 | class CheckwattSensor(AbstractCheckwattSensor): 250 | """Representation of a CheckWatt sensor.""" 251 | 252 | def __init__( 253 | self, 254 | coordinator: CheckwattCoordinator, 255 | description: SensorEntityDescription, 256 | ) -> None: 257 | """Initialize the sensor.""" 258 | super().__init__(coordinator=coordinator, description=description) 259 | self._attr_unique_id = f'checkwattUid_{self._coordinator.data["id"]}' 260 | 261 | self._attr_extra_state_attributes = {} 262 | if "display_name" in self._coordinator.data: 263 | self._attr_extra_state_attributes.update( 264 | {C_DISPLAY_NAME: self._coordinator.data["display_name"]} 265 | ) 266 | if "address" in self._coordinator.data: 267 | self._attr_extra_state_attributes.update( 268 | {C_ADR: self._coordinator.data["address"]} 269 | ) 270 | if "zip" in self._coordinator.data: 271 | self._attr_extra_state_attributes.update( 272 | {C_ZIP: self._coordinator.data["zip"]} 273 | ) 274 | if "city" in self._coordinator.data: 275 | self._attr_extra_state_attributes.update( 276 | {C_CITY: self._coordinator.data["city"]} 277 | ) 278 | if "dso" in self._coordinator.data: 279 | self._attr_extra_state_attributes.update( 280 | {C_DSO: self._coordinator.data["dso"]} 281 | ) 282 | if "energy_provider" in self._coordinator.data: 283 | self._attr_extra_state_attributes.update( 284 | {C_ENERGY_PROVIDER: self._coordinator.data["energy_provider"]} 285 | ) 286 | if "tomorrow_net_revenue" in self._coordinator.data: 287 | self._attr_extra_state_attributes.update( 288 | {C_TOMORROW_REVENUE: self._coordinator.data["tomorrow_net_revenue"]} 289 | ) 290 | if "update_time" in self._coordinator.data: 291 | self._attr_extra_state_attributes.update( 292 | {C_UPDATE_TIME: self._coordinator.data["update_time"]} 293 | ) 294 | if "next_update_time" in self._coordinator.data: 295 | self._attr_extra_state_attributes.update( 296 | {C_NEXT_UPDATE_TIME: self._coordinator.data["next_update_time"]} 297 | ) 298 | 299 | self._attr_available = False 300 | 301 | async def async_update(self) -> None: 302 | """Get the latest data and updates the states.""" 303 | self._attr_available = True 304 | 305 | @callback 306 | def _handle_coordinator_update(self) -> None: 307 | """Get the latest data and updates the states.""" 308 | if "update_time" in self._coordinator.data: 309 | self._attr_extra_state_attributes.update( 310 | {C_UPDATE_TIME: self._coordinator.data["update_time"]} 311 | ) 312 | if "next_update_time" in self._coordinator.data: 313 | self._attr_extra_state_attributes.update( 314 | {C_NEXT_UPDATE_TIME: self._coordinator.data["next_update_time"]} 315 | ) 316 | if "tomorrow_net_revenue" in self._coordinator.data: 317 | self._attr_extra_state_attributes.update( 318 | {C_TOMORROW_REVENUE: self._coordinator.data["tomorrow_net_revenue"]} 319 | ) 320 | super()._handle_coordinator_update() 321 | 322 | @property 323 | def native_value(self) -> str | None: 324 | """Get the latest state value.""" 325 | if "today_net_revenue" in self._coordinator.data: 326 | return round(self._coordinator.data["today_net_revenue"], 2) 327 | return None 328 | 329 | 330 | class CheckwattMonthlySensor(AbstractCheckwattSensor): 331 | """Representation of a CheckWatt Monthly Revenue sensor.""" 332 | 333 | def __init__( 334 | self, 335 | coordinator: CheckwattCoordinator, 336 | description: SensorEntityDescription, 337 | ) -> None: 338 | """Initialize the sensor.""" 339 | super().__init__(coordinator=coordinator, description=description) 340 | self._attr_unique_id = f'checkwattUid_Monthly_{self._coordinator.data["id"]}' 341 | self._attr_extra_state_attributes = {} 342 | self._attr_available = False 343 | 344 | async def async_update(self) -> None: 345 | """Get the latest data and updates the states.""" 346 | if "month_estimate" in self._coordinator.data: 347 | self._attr_extra_state_attributes.update( 348 | {C_MONTH_ESITIMATE: round(self._coordinator.data["month_estimate"], 2)} 349 | ) 350 | if "daily_average" in self._coordinator.data: 351 | self._attr_extra_state_attributes.update( 352 | {C_DAILY_AVERAGE: round(self._coordinator.data["daily_average"], 2)} 353 | ) 354 | self._attr_available = True 355 | 356 | @callback 357 | def _handle_coordinator_update(self) -> None: 358 | """Get the latest data and updates the states.""" 359 | if "monthly_net_revenue" in self._coordinator.data: 360 | self._attr_native_value = round( 361 | self._coordinator.data["monthly_net_revenue"], 2 362 | ) 363 | if "month_estimate" in self._coordinator.data: 364 | self._attr_extra_state_attributes.update( 365 | {C_MONTH_ESITIMATE: round(self._coordinator.data["month_estimate"], 2)} 366 | ) 367 | if "daily_average" in self._coordinator.data: 368 | self._attr_extra_state_attributes.update( 369 | {C_DAILY_AVERAGE: round(self._coordinator.data["daily_average"], 2)} 370 | ) 371 | super()._handle_coordinator_update() 372 | 373 | @property 374 | def native_value(self) -> str | None: 375 | """Get the latest state value.""" 376 | if "monthly_net_revenue" in self._coordinator.data: 377 | return round(self._coordinator.data["monthly_net_revenue"], 2) 378 | return None 379 | 380 | 381 | class CheckwattAnnualSensor(AbstractCheckwattSensor): 382 | """Representation of a CheckWatt Annual Revenue sensor.""" 383 | 384 | def __init__( 385 | self, 386 | coordinator: CheckwattCoordinator, 387 | description: SensorEntityDescription, 388 | ) -> None: 389 | """Initialize the sensor.""" 390 | super().__init__(coordinator=coordinator, description=description) 391 | self._attr_unique_id = f'checkwattUid_Annual_{self._coordinator.data["id"]}' 392 | self._attr_extra_state_attributes = {} 393 | self._attr_available = False 394 | 395 | async def async_update(self) -> None: 396 | """Get the latest data and updates the states.""" 397 | self._attr_available = False 398 | 399 | @callback 400 | def _handle_coordinator_update(self) -> None: 401 | """Get the latest data and updates the states.""" 402 | if "annual_net_revenue" in self._coordinator.data: 403 | self._attr_native_value = round( 404 | self._coordinator.data["annual_net_revenue"], 2 405 | ) 406 | super()._handle_coordinator_update() 407 | 408 | @property 409 | def native_value(self) -> str | None: 410 | """Get the latest state value.""" 411 | if "annual_net_revenue" in self._coordinator.data: 412 | return round(self._coordinator.data["annual_net_revenue"], 2) 413 | return None 414 | 415 | 416 | class CheckwattEnergySensor(AbstractCheckwattSensor): 417 | """Representation of a CheckWatt Energy sensor.""" 418 | 419 | def __init__( 420 | self, 421 | coordinator: CheckwattCoordinator, 422 | description: SensorEntityDescription, 423 | data_key, 424 | ) -> None: 425 | """Initialize the sensor.""" 426 | super().__init__(coordinator=coordinator, description=description) 427 | self.data_key = data_key 428 | 429 | async def async_update(self) -> None: 430 | """Get the latest data and updates the states.""" 431 | self._attr_available = True 432 | 433 | @callback 434 | def _handle_coordinator_update(self) -> None: 435 | """Get the latest data and updates the states.""" 436 | self._attr_native_value = round(self._coordinator.data[self.data_key] / 1000, 2) 437 | super()._handle_coordinator_update() 438 | 439 | @property 440 | def native_value(self) -> str | None: 441 | """Get the latest state value.""" 442 | return round(self._coordinator.data[self.data_key] / 1000, 2) 443 | 444 | 445 | class CheckwattSpotPriceSensor(AbstractCheckwattSensor): 446 | """Representation of a CheckWatt Spot-price sensor.""" 447 | 448 | def __init__( 449 | self, 450 | coordinator: CheckwattCoordinator, 451 | description: SensorEntityDescription, 452 | vat_key, 453 | ) -> None: 454 | """Initialize the sensor.""" 455 | super().__init__(coordinator=coordinator, description=description) 456 | self.vat_key = vat_key 457 | 458 | async def async_update(self) -> None: 459 | """Get the latest data and updates the states.""" 460 | if "price_zone" in self._coordinator.data: 461 | self._attr_extra_state_attributes.update( 462 | {C_PRICE_ZONE: self._coordinator.data["price_zone"]} 463 | ) 464 | if self.vat_key == "inc_vat": 465 | self._attr_extra_state_attributes.update({C_VAT: "25%"}) 466 | self._attr_available = True 467 | 468 | @callback 469 | def _handle_coordinator_update(self) -> None: 470 | """Get the latest data and updates the states.""" 471 | if self.vat_key == "inc_vat": 472 | self._attr_native_value = round( 473 | self._coordinator.data["spot_price"] * 1.25, 3 474 | ) 475 | else: 476 | self._attr_native_value = self._coordinator.data["spot_price"] 477 | super()._handle_coordinator_update() 478 | 479 | @property 480 | def native_value(self) -> str | None: 481 | """Get the latest state value.""" 482 | if self.vat_key == "inc_vat": 483 | return round(self._coordinator.data["spot_price"] * 1.25, 3) 484 | return round(self._coordinator.data["spot_price"], 3) 485 | 486 | 487 | class CheckwattBatterySoCSensor(AbstractCheckwattSensor): 488 | """Representation of a CheckWatt Battery SoC sensor.""" 489 | 490 | def __init__( 491 | self, 492 | coordinator: CheckwattCoordinator, 493 | description: SensorEntityDescription, 494 | ) -> None: 495 | """Initialize the sensor.""" 496 | super().__init__(coordinator=coordinator, description=description) 497 | 498 | async def async_update(self) -> None: 499 | """Get the latest data and updates the states.""" 500 | if "battery_power" in self._coordinator.data: 501 | self._attr_extra_state_attributes.update( 502 | {C_BATTERY_POWER: self._coordinator.data["battery_power"]} 503 | ) 504 | if "grid_power" in self._coordinator.data: 505 | self._attr_extra_state_attributes.update( 506 | {C_GRID_POWER: self._coordinator.data["grid_power"]} 507 | ) 508 | if "solar_power" in self._coordinator.data: 509 | self._attr_extra_state_attributes.update( 510 | {C_SOLAR_POWER: self._coordinator.data["solar_power"]} 511 | ) 512 | if "charge_peak_ac" in self._coordinator.data: 513 | self._attr_extra_state_attributes.update( 514 | {C_CHARGE_PEAK_AC: self._coordinator.data["charge_peak_ac"]} 515 | ) 516 | if "charge_peak_dc" in self._coordinator.data: 517 | self._attr_extra_state_attributes.update( 518 | {C_CHARGE_PEAK_DC: self._coordinator.data["charge_peak_dc"]} 519 | ) 520 | if "discharge_peak_ac" in self._coordinator.data: 521 | self._attr_extra_state_attributes.update( 522 | {C_DISCHARGE_PEAK_AC: self._coordinator.data["discharge_peak_ac"]} 523 | ) 524 | if "discharge_peak_dc" in self._coordinator.data: 525 | self._attr_extra_state_attributes.update( 526 | {C_DISCHARGE_PEAK_DC: self._coordinator.data["discharge_peak_dc"]} 527 | ) 528 | if "monthly_grid_peak_power" in self._coordinator.data: 529 | self._attr_extra_state_attributes.update( 530 | { 531 | C_MONTHLY_GRID_PEAK_POWER: self._coordinator.data[ 532 | "monthly_grid_peak_power" 533 | ] 534 | } 535 | ) 536 | self._attr_available = True 537 | 538 | @callback 539 | def _handle_coordinator_update(self) -> None: 540 | """Get the latest data and updates the states.""" 541 | if "battery_soc" in self._coordinator.data: 542 | self._attr_native_value = self._coordinator.data["battery_soc"] 543 | if "battery_power" in self._coordinator.data: 544 | self._attr_extra_state_attributes.update( 545 | {C_BATTERY_POWER: self._coordinator.data["battery_power"]} 546 | ) 547 | if "grid_power" in self._coordinator.data: 548 | self._attr_extra_state_attributes.update( 549 | {C_GRID_POWER: self._coordinator.data["grid_power"]} 550 | ) 551 | if "solar_power" in self._coordinator.data: 552 | self._attr_extra_state_attributes.update( 553 | {C_SOLAR_POWER: self._coordinator.data["solar_power"]} 554 | ) 555 | if "charge_peak_ac" in self._coordinator.data: 556 | self._attr_extra_state_attributes.update( 557 | {C_CHARGE_PEAK_AC: self._coordinator.data["charge_peak_ac"]} 558 | ) 559 | if "charge_peak_dc" in self._coordinator.data: 560 | self._attr_extra_state_attributes.update( 561 | {C_CHARGE_PEAK_DC: self._coordinator.data["charge_peak_dc"]} 562 | ) 563 | if "discharge_peak_ac" in self._coordinator.data: 564 | self._attr_extra_state_attributes.update( 565 | {C_DISCHARGE_PEAK_AC: self._coordinator.data["discharge_peak_ac"]} 566 | ) 567 | if "discharge_peak_dc" in self._coordinator.data: 568 | self._attr_extra_state_attributes.update( 569 | {C_DISCHARGE_PEAK_DC: self._coordinator.data["discharge_peak_dc"]} 570 | ) 571 | if "monthly_grid_peak_power" in self._coordinator.data: 572 | self._attr_extra_state_attributes.update( 573 | { 574 | C_MONTHLY_GRID_PEAK_POWER: self._coordinator.data[ 575 | "monthly_grid_peak_power" 576 | ] 577 | } 578 | ) 579 | super()._handle_coordinator_update() 580 | 581 | @property 582 | def native_value(self) -> str | None: 583 | """Get the latest state value.""" 584 | return self._coordinator.data["battery_soc"] 585 | 586 | 587 | class CheckwattCM10Sensor(AbstractCheckwattSensor): 588 | """Representation of a CheckWatt CM10 sensor.""" 589 | 590 | def __init__( 591 | self, 592 | coordinator: CheckwattCoordinator, 593 | description: SensorEntityDescription, 594 | ) -> None: 595 | """Initialize the sensor.""" 596 | super().__init__(coordinator=coordinator, description=description) 597 | 598 | async def async_update(self) -> None: 599 | """Get the latest data and updates the states.""" 600 | if "cm10_version" in self._coordinator.data: 601 | self._attr_extra_state_attributes.update( 602 | {C_CM10_VERSION: self._coordinator.data["cm10_version"]} 603 | ) 604 | if "fcr_d_status" in self._coordinator.data: 605 | self._attr_extra_state_attributes.update( 606 | {C_FCRD_STATUS: self._coordinator.data["fcr_d_status"]} 607 | ) 608 | if "fcr_d_info" in self._coordinator.data: 609 | self._attr_extra_state_attributes.update( 610 | {C_FCRD_INFO: self._coordinator.data["fcr_d_info"]} 611 | ) 612 | if "fcr_d_date" in self._coordinator.data: 613 | self._attr_extra_state_attributes.update( 614 | {C_FCRD_DATE: self._coordinator.data["fcr_d_date"]} 615 | ) 616 | if "reseller_id" in self._coordinator.data: 617 | self._attr_extra_state_attributes.update( 618 | {C_RESELLER_ID: self._coordinator.data["reseller_id"]} 619 | ) 620 | self._attr_available = True 621 | 622 | @callback 623 | def _handle_coordinator_update(self) -> None: 624 | """Get the latest data and updates the states.""" 625 | if "cm10_status" in self._coordinator.data: 626 | cm10_status = self._coordinator.data["cm10_status"] 627 | if cm10_status is not None: 628 | self._attr_native_value = cm10_status.capitalize() 629 | else: 630 | self._attr_native_value = None 631 | if "cm10_version" in self._coordinator.data: 632 | self._attr_extra_state_attributes.update( 633 | {C_CM10_VERSION: self._coordinator.data["cm10_version"]} 634 | ) 635 | if "fcr_d_status" in self._coordinator.data: 636 | self._attr_extra_state_attributes.update( 637 | {C_FCRD_STATUS: self._coordinator.data["fcr_d_status"]} 638 | ) 639 | if "fcr_d_info" in self._coordinator.data: 640 | self._attr_extra_state_attributes.update( 641 | {C_FCRD_INFO: self._coordinator.data["fcr_d_info"]} 642 | ) 643 | if "fcr_d_date" in self._coordinator.data: 644 | self._attr_extra_state_attributes.update( 645 | {C_FCRD_DATE: self._coordinator.data["fcr_d_date"]} 646 | ) 647 | if "reseller_id" in self._coordinator.data: 648 | self._attr_extra_state_attributes.update( 649 | {C_RESELLER_ID: self._coordinator.data["reseller_id"]} 650 | ) 651 | super()._handle_coordinator_update() 652 | 653 | @property 654 | def native_value(self) -> str | None: 655 | """Get the latest state value.""" 656 | if "cm10_status" in self._coordinator.data: 657 | cm10_status = self._coordinator.data["cm10_status"] 658 | if cm10_status is not None: 659 | return cm10_status.capitalize() 660 | return None 661 | -------------------------------------------------------------------------------- /custom_components/checkwatt/services.yaml: -------------------------------------------------------------------------------- 1 | update_history: 2 | name: "Update CheckWattRank History" 3 | description: "Updates CheckWattRank with historical data from EnergyInBalances." 4 | fields: 5 | start_date: 6 | name: "Start date" 7 | description: "The start date to fetch history from. Max 6 months back." 8 | required: true 9 | example: "2023-12-01" 10 | selector: 11 | date: 12 | end_date: 13 | name: "End date" 14 | description: "The end date to fetch history from. Max 6 months back." 15 | required: true 16 | example: "2024-01-01" 17 | selector: 18 | date: 19 | 20 | push_checkwatt_rank: 21 | name: "Push Today's Revenue to CheckWattRank" 22 | description: "Updates CheckWattRank with current revenues from EnergyInBalances." 23 | -------------------------------------------------------------------------------- /custom_components/checkwatt/strings.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "data": { 6 | "username": "[%key:common::config_flow::data::username%]", 7 | "password": "[%key:common::config_flow::data::password%]" 8 | }, 9 | "description": "Please enter the username and password for your CheckWatt EnergyInBalance account", 10 | "title": "CheckWatt" 11 | } 12 | }, 13 | "error": { 14 | "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", 15 | "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", 16 | "unknown": "[%key:common::config_flow::error::unknown%]" 17 | }, 18 | "abort": { 19 | "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" 20 | } 21 | }, 22 | "options": { 23 | "step": { 24 | "init": { 25 | "data": { 26 | "show_details": "Provide energy sensors", 27 | "push_to_cw_rank": "Push data to CheckWattRank", 28 | "cm10_sensor": "Provide CM10 sensor", 29 | "cwr_name": "System name for CheckWattRank" 30 | }, 31 | "description": "Select options", 32 | "title": "CheckWatt" 33 | } 34 | } 35 | }, 36 | "entity": { 37 | "sensor": { 38 | "daily_yield_sensor": { 39 | "name": "CheckWatt Daily Net Income", 40 | "state_attributes": { 41 | "display_name": { 42 | "name": "Facility" 43 | }, 44 | "street_address": { 45 | "name": "Street Address" 46 | }, 47 | "zip_code": { 48 | "name": "Zip Code" 49 | }, 50 | "city": { 51 | "name": "City" 52 | }, 53 | "dso": { 54 | "name": "Distribution System Operator" 55 | }, 56 | "energy_provider": { 57 | "name": "Energy Provider" 58 | }, 59 | "tomorrow_net_revenue": { 60 | "name": "Tomorrow Net Income" 61 | }, 62 | "last_update": { 63 | "name": "Last update" 64 | }, 65 | "next_update": { 66 | "name": "Next update" 67 | } 68 | } 69 | }, 70 | "monthly_yield_sensor": { 71 | "name": "CheckWatt Monthly Net Income", 72 | "state_attributes": { 73 | "month_estimate": { 74 | "name": "Month Estimate" 75 | }, 76 | "daily_average": { 77 | "name": "Daily Average" 78 | } 79 | } 80 | }, 81 | "annual_yield_sensor": { 82 | "name": "CheckWatt Annual Net Income", 83 | "state_attributes": {} 84 | }, 85 | "solar_sensor": { 86 | "name": "Solar Energy" 87 | }, 88 | "charging_sensor": { 89 | "name": "Battery Charging Energy" 90 | }, 91 | "discharging_sensor": { 92 | "name": "Battery Discharging Energy" 93 | }, 94 | "import_sensor": { 95 | "name": "Import Energy" 96 | }, 97 | "export_sensor": { 98 | "name": "Export Energy" 99 | }, 100 | "spot_price_sensor": { 101 | "name": "Spot Price", 102 | "state_attributes": { 103 | "prize_zone": { 104 | "name": "Price zone" 105 | } 106 | } 107 | }, 108 | "spot_price_vat_sensor": { 109 | "name": "Spot Price incl. VAT", 110 | "state_attributes": { 111 | "prize_zone": { 112 | "name": "Price zone" 113 | }, 114 | "vat": { 115 | "name": "VAT" 116 | } 117 | } 118 | }, 119 | "battery_soc_sensor": { 120 | "name": "Battery SoC", 121 | "state_attributes": { 122 | "battery_power": { 123 | "name": "Battery Power" 124 | }, 125 | "grid_power": { 126 | "name": "Grid Power" 127 | }, 128 | "solar_power": { 129 | "name": "Solar Power" 130 | }, 131 | "charge_peak_ac": { 132 | "name": "Charge Peak AC" 133 | }, 134 | "charge_peak_dc": { 135 | "name": "Charge Peak DC" 136 | }, 137 | "discharge_peak_ac": { 138 | "name": "Discharge Peak AC" 139 | }, 140 | "discharge_peak_dc": { 141 | "name": "Discharge Peak DC" 142 | }, 143 | "monthly_grid_peak_power": { 144 | "name": "Montly Peak Grid Power" 145 | } 146 | } 147 | }, 148 | "cm10_sensor": { 149 | "name": "CheckWatt CM10 Status", 150 | "state_attributes": { 151 | "cm10_version": { 152 | "name": "Version" 153 | }, 154 | "fcr_d_info": { 155 | "name": "FCR-D Info" 156 | }, 157 | "fcr_d_status": { 158 | "name": "FCR-D Status" 159 | }, 160 | "fcr_d_date": { 161 | "name": "FCR-D Date" 162 | }, 163 | "reseller_id": { 164 | "name": "Partner Id" 165 | } 166 | } 167 | } 168 | }, 169 | "event": { 170 | "fcr_d_event": { 171 | "name": "FCR-D State", 172 | "state_attributes": { 173 | "event_type": { 174 | "state": { 175 | "fcrd_activated": "Activated", 176 | "fcrd_deactivated": "Deactivated", 177 | "fcrd_failed": "Failed" 178 | } 179 | } 180 | } 181 | } 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /custom_components/checkwatt/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "data": { 6 | "username": "Username", 7 | "password": "Password" 8 | }, 9 | "description": "Please enter the username and password for your CheckWatt EnergyInBalance account", 10 | "title":"CheckWatt" 11 | } 12 | }, 13 | "error": { 14 | "cannot_connect": "Cannot connect to CheckWatt", 15 | "invalid_auth": "Failed to authorize", 16 | "unknown": "Unknown error" 17 | }, 18 | "abort": { 19 | "already_configured": "Already configured" 20 | } 21 | }, 22 | "options": { 23 | "step": { 24 | "init": { 25 | "data": { 26 | "show_details": "Provide energy sensors", 27 | "push_to_cw_rank": "Push data to CheckWattRank", 28 | "cm10_sensor": "Provide CM10 sensor", 29 | "cwr_name": "System name for CheckWattRank" 30 | }, 31 | "description": "Select options", 32 | "title": "CheckWatt" 33 | } 34 | } 35 | }, 36 | "entity": { 37 | "sensor": { 38 | "daily_yield_sensor": { 39 | "name": "CheckWatt Daily Net Income", 40 | "state_attributes": { 41 | "display_name": { 42 | "name": "Facility" 43 | }, 44 | "street_address": { 45 | "name": "Street Address" 46 | }, 47 | "zip_code": { 48 | "name": "Zip Code" 49 | }, 50 | "city": { 51 | "name": "City" 52 | }, 53 | "dso": { 54 | "name": "Distribution System Operator" 55 | }, 56 | "energy_provider": { 57 | "name": "Energy Provider" 58 | }, 59 | "tomorrow_net_revenue": { 60 | "name": "Tomorrow Net Income" 61 | }, 62 | "last_update": { 63 | "name": "Last update" 64 | }, 65 | "next_update": { 66 | "name": "Next update" 67 | } 68 | } 69 | }, 70 | "monthly_yield_sensor": { 71 | "name": "CheckWatt Monthly Net Income", 72 | "state_attributes": { 73 | "month_estimate": { 74 | "name": "Month Estimate" 75 | }, 76 | "daily_average": { 77 | "name": "Daily Average" 78 | } 79 | } 80 | }, 81 | "annual_yield_sensor": { 82 | "name": "CheckWatt Annual Net Income", 83 | "state_attributes": {} 84 | }, 85 | "solar_sensor": { 86 | "name": "Solar Energy" 87 | }, 88 | "charging_sensor": { 89 | "name": "Battery Charging Energy" 90 | }, 91 | "discharging_sensor": { 92 | "name": "Battery Discharging Energy" 93 | }, 94 | "import_sensor": { 95 | "name": "Import Energy" 96 | }, 97 | "export_sensor": { 98 | "name": "Export Energy" 99 | }, 100 | "spot_price_sensor": { 101 | "name": "Spot Price", 102 | "state_attributes": { 103 | "prize_zone": { 104 | "name": "Price zone" 105 | } 106 | } 107 | }, 108 | "spot_price_vat_sensor": { 109 | "name": "Spot Price incl. VAT", 110 | "state_attributes": { 111 | "prize_zone": { 112 | "name": "Price zone" 113 | }, 114 | "vat": { 115 | "name": "VAT" 116 | } 117 | } 118 | }, 119 | "battery_soc_sensor": { 120 | "name": "Battery SoC", 121 | "state_attributes": { 122 | "battery_power": { 123 | "name": "Battery Power" 124 | }, 125 | "grid_power": { 126 | "name": "Grid Power" 127 | }, 128 | "solar_power": { 129 | "name": "Solar Power" 130 | }, 131 | "charge_peak_ac": { 132 | "name": "Charge Peak AC" 133 | }, 134 | "charge_peak_dc": { 135 | "name": "Charge Peak DC" 136 | }, 137 | "discharge_peak_ac": { 138 | "name": "Discharge Peak AC" 139 | }, 140 | "discharge_peak_dc": { 141 | "name": "Discharge Peak DC" 142 | }, 143 | "monthly_grid_peak_power": { 144 | "name": "Montly Peak Grid Power" 145 | } 146 | } 147 | }, 148 | "cm10_sensor": { 149 | "name": "CheckWatt CM10 Status", 150 | "state_attributes": { 151 | "cm10_version": { 152 | "name": "Version" 153 | }, 154 | "fcr_d_info": { 155 | "name": "FCR-D Info" 156 | }, 157 | "fcr_d_status": { 158 | "name": "FCR-D Status" 159 | }, 160 | "fcr_d_date": { 161 | "name": "FCR-D Date" 162 | }, 163 | "reseller_id": { 164 | "name": "Partner Id" 165 | } 166 | } 167 | } 168 | }, 169 | "event": { 170 | "fcr_d_event": { 171 | "name": "FCR-D State", 172 | "state_attributes": { 173 | "event_type": { 174 | "state": { 175 | "fcrd_activated": "Activated", 176 | "fcrd_deactivated": "Deactivated", 177 | "fcrd_failed": "Failed" 178 | } 179 | } 180 | } 181 | } 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /custom_components/checkwatt/translations/sv.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "data": { 6 | "username": "Användarnamn", 7 | "password": "Lösenord" 8 | }, 9 | "description": "Ange namn och lösen till ditt CheckWatt EnergyInBalance konto", 10 | "title":"CheckWatt" 11 | } 12 | }, 13 | "error": { 14 | "cannot_connect": "Kan inte ansluta", 15 | "invalid_auth": "Kan inte authentisera", 16 | "unknown": "Okänt fel" 17 | }, 18 | "abort": { 19 | "already_configured": "Redan konfigurerad" 20 | } 21 | }, 22 | "options": { 23 | "step": { 24 | "init": { 25 | "data": { 26 | "show_details": "Skapa energisensorer", 27 | "push_to_cw_rank": "Skicka data till CheckWattRank", 28 | "cm10_sensor": "Skapa CM10 sensor", 29 | "cwr_name": "Systemnamn till CheckWattRank" 30 | }, 31 | "description": "Dina val", 32 | "title": "CheckWatt" 33 | } 34 | } 35 | }, 36 | "entity": { 37 | "sensor": { 38 | "daily_yield_sensor": { 39 | "name": "CheckWatt Daglig Intäkt", 40 | "state_attributes": { 41 | "display_name": { 42 | "name": "Anläggning" 43 | }, 44 | "street_address": { 45 | "name": "Adress" 46 | }, 47 | "zip_code": { 48 | "name": "Postnummer" 49 | }, 50 | "city": { 51 | "name": "Stad" 52 | }, 53 | "dso": { 54 | "name": "Nätägare" 55 | }, 56 | "energy_provider": { 57 | "name": "Elhandelsbolag" 58 | }, 59 | "tomorrow_net_revenue": { 60 | "name": "Intäkt imorgon" 61 | }, 62 | "last_update": { 63 | "name": "Senaste uppdateringen" 64 | }, 65 | "next_update": { 66 | "name": "Nästa uppdatering" 67 | } 68 | } 69 | }, 70 | "monthly_yield_sensor": { 71 | "name": "CheckWatt Månadens Intäkt", 72 | "state_attributes": { 73 | "month_estimate": { 74 | "name": "Månadsestimat" 75 | }, 76 | "daily_average": { 77 | "name": "Daglig medelintäkt" 78 | } 79 | } 80 | }, 81 | "annual_yield_sensor": { 82 | "name": "CheckWatt Årets Intäkt", 83 | "state_attributes": {} 84 | }, 85 | "solar_sensor": { 86 | "name": "Solenergi" 87 | }, 88 | "charging_sensor": { 89 | "name": "Batteri Laddning Energi" 90 | }, 91 | "discharging_sensor": { 92 | "name": "Batteri Urladdning Energi" 93 | }, 94 | "import_sensor": { 95 | "name": "Importerad Energi" 96 | }, 97 | "export_sensor": { 98 | "name": "Exporterad Energi" 99 | }, 100 | "spot_price_sensor": { 101 | "name": "Spotpris", 102 | "state_attributes": { 103 | "prize_zone": { 104 | "name": "Elprisområde" 105 | } 106 | } 107 | }, 108 | "spot_price_vat_sensor": { 109 | "name": "Spotpris inkl. moms", 110 | "state_attributes": { 111 | "prize_zone": { 112 | "name": "Elprisområde" 113 | }, 114 | "vat": { 115 | "name": "Moms" 116 | } 117 | } 118 | }, 119 | "battery_soc_sensor": { 120 | "name": "Batteriladdning", 121 | "state_attributes": { 122 | "battery_power": { 123 | "name": "Batterieffekt" 124 | }, 125 | "grid_power": { 126 | "name": "Näteffekt" 127 | }, 128 | "solar_power": { 129 | "name": "Solcellseffekt" 130 | }, 131 | "charge_peak_ac": { 132 | "name": "Laddningstopp AC" 133 | }, 134 | "charge_peak_dc": { 135 | "name": "Laddningstopp DC" 136 | }, 137 | "discharge_peak_ac": { 138 | "name": "Urladdningstopp AC" 139 | }, 140 | "discharge_peak_dc": { 141 | "name": "Urladdningstopp DC" 142 | }, 143 | "monthly_grid_peak_power": { 144 | "name": "Månadens maximala näteffekt" 145 | } 146 | } 147 | }, 148 | "cm10_sensor": { 149 | "name": "CheckWatt CM10 Status", 150 | "state_attributes": { 151 | "cm10_version": { 152 | "name": "Version" 153 | }, 154 | "fcr_d_info": { 155 | "name": "FCR-D Info" 156 | }, 157 | "fcr_d_status": { 158 | "name": "FCR-D Status" 159 | }, 160 | "fcr_d_date": { 161 | "name": "FCR-D Datum" 162 | }, 163 | "reseller_id": { 164 | "name": "Partner Id" 165 | } 166 | } 167 | } 168 | }, 169 | "event": { 170 | "fcr_d_event": { 171 | "name": "FCR-D Status", 172 | "state_attributes": { 173 | "event_type": { 174 | "state": { 175 | "fcrd_activated": "Aktiverad", 176 | "fcrd_deactivated": "Deaktiverad", 177 | "fcrd_failed": "Misslyckades" 178 | } 179 | } 180 | } 181 | } 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CheckWatt", 3 | "homeassistant": "2023.12.3", 4 | "render_readme": true, 5 | "country": ["ALL"] 6 | } 7 | -------------------------------------------------------------------------------- /images/basic_sensor_annual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faanskit/ha-checkwatt/9215a7da5f7ed853cb98846a9de8dd6cce907358/images/basic_sensor_annual.png -------------------------------------------------------------------------------- /images/basic_sensor_daily.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faanskit/ha-checkwatt/9215a7da5f7ed853cb98846a9de8dd6cce907358/images/basic_sensor_daily.png -------------------------------------------------------------------------------- /images/configure_done.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faanskit/ha-checkwatt/9215a7da5f7ed853cb98846a9de8dd6cce907358/images/configure_done.png -------------------------------------------------------------------------------- /images/configure_step_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faanskit/ha-checkwatt/9215a7da5f7ed853cb98846a9de8dd6cce907358/images/configure_step_1.png -------------------------------------------------------------------------------- /images/configure_step_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faanskit/ha-checkwatt/9215a7da5f7ed853cb98846a9de8dd6cce907358/images/configure_step_2.png -------------------------------------------------------------------------------- /images/configure_step_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faanskit/ha-checkwatt/9215a7da5f7ed853cb98846a9de8dd6cce907358/images/configure_step_3.png -------------------------------------------------------------------------------- /images/detailed_sensor_annual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faanskit/ha-checkwatt/9215a7da5f7ed853cb98846a9de8dd6cce907358/images/detailed_sensor_annual.png -------------------------------------------------------------------------------- /images/detailed_sensor_daily.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faanskit/ha-checkwatt/9215a7da5f7ed853cb98846a9de8dd6cce907358/images/detailed_sensor_daily.png -------------------------------------------------------------------------------- /images/dev_tools_states.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faanskit/ha-checkwatt/9215a7da5f7ed853cb98846a9de8dd6cce907358/images/dev_tools_states.png -------------------------------------------------------------------------------- /images/energy_done.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faanskit/ha-checkwatt/9215a7da5f7ed853cb98846a9de8dd6cce907358/images/energy_done.png -------------------------------------------------------------------------------- /images/energy_step_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faanskit/ha-checkwatt/9215a7da5f7ed853cb98846a9de8dd6cce907358/images/energy_step_1.png -------------------------------------------------------------------------------- /images/energy_step_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faanskit/ha-checkwatt/9215a7da5f7ed853cb98846a9de8dd6cce907358/images/energy_step_2.png -------------------------------------------------------------------------------- /images/energy_step_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faanskit/ha-checkwatt/9215a7da5f7ed853cb98846a9de8dd6cce907358/images/energy_step_3.png -------------------------------------------------------------------------------- /images/energy_step_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faanskit/ha-checkwatt/9215a7da5f7ed853cb98846a9de8dd6cce907358/images/energy_step_4.png -------------------------------------------------------------------------------- /images/energy_step_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faanskit/ha-checkwatt/9215a7da5f7ed853cb98846a9de8dd6cce907358/images/energy_step_5.png -------------------------------------------------------------------------------- /images/energy_step_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faanskit/ha-checkwatt/9215a7da5f7ed853cb98846a9de8dd6cce907358/images/energy_step_6.png -------------------------------------------------------------------------------- /images/expert_sensor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faanskit/ha-checkwatt/9215a7da5f7ed853cb98846a9de8dd6cce907358/images/expert_sensor.png -------------------------------------------------------------------------------- /images/ha_main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faanskit/ha-checkwatt/9215a7da5f7ed853cb98846a9de8dd6cce907358/images/ha_main.png -------------------------------------------------------------------------------- /images/options_done.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faanskit/ha-checkwatt/9215a7da5f7ed853cb98846a9de8dd6cce907358/images/options_done.png -------------------------------------------------------------------------------- /images/options_step_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faanskit/ha-checkwatt/9215a7da5f7ed853cb98846a9de8dd6cce907358/images/options_step_1.png -------------------------------------------------------------------------------- /images/options_step_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faanskit/ha-checkwatt/9215a7da5f7ed853cb98846a9de8dd6cce907358/images/options_step_2.png -------------------------------------------------------------------------------- /images/options_step_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faanskit/ha-checkwatt/9215a7da5f7ed853cb98846a9de8dd6cce907358/images/options_step_3.png -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = .venv,.git,docs,venv,bin,lib,deps,build 3 | max-complexity = 25 4 | doctests = True 5 | # To work with Black 6 | # E501: line too long 7 | # W503: Line break occurred before a binary operator 8 | # E203: Whitespace before ':' 9 | # D202 No blank lines allowed after function docstring 10 | # W504 line break after binary operator 11 | ignore = 12 | E501, 13 | W503, 14 | E203, 15 | D202, 16 | W504 17 | noqa-require-code = True 18 | --------------------------------------------------------------------------------