├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── codeql-analysis.yml │ └── hassfest.yaml ├── .gitignore ├── LICENSE ├── README.md ├── configuration.yaml ├── custom_components └── goecharger │ ├── __init__.py │ ├── config_flow.py │ ├── const.py │ ├── manifest.json │ ├── sensor.py │ ├── services.yaml │ ├── switch.py │ └── translations │ ├── de.json │ └── en.json ├── doc └── ha_entity_view.png ├── docker-compose.yaml └── hacs.json /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Versions (please complete the following information):** 27 | - home-assistant [] 28 | - Charger Hardware: [e.g. v2] 29 | - Charger Firmware Version: [e.g. 0.40] 30 | - Plugin Version: [e.g. 0.23] 31 | 32 | **Additional context** 33 | Add any other context about the problem here. 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '43 14 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v2 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v2 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v2 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v2 72 | -------------------------------------------------------------------------------- /.github/workflows/hassfest.yaml: -------------------------------------------------------------------------------- 1 | name: Validate with hassfest 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: "0 0 * * *" 8 | 9 | jobs: 10 | validate: 11 | runs-on: "ubuntu-latest" 12 | steps: 13 | - uses: "actions/checkout@v2" 14 | - uses: home-assistant/actions/hassfest@master 15 | -------------------------------------------------------------------------------- /.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 | .DS_Store 131 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2022 Carsten Thiele 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Home Assistant integration for the go-eCharger (WIP) 2 | 3 | [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/custom-components/hacs) 4 | [![Validate with hassfest](https://github.com/cathiele/homeassistant-goecharger/actions/workflows/hassfest.yaml/badge.svg?branch=main)](https://github.com/cathiele/homeassistant-goecharger/actions/workflows/hassfest.yaml) 5 | 6 | Integration for Homeassistant to view and Control the go-eCharger for electric Vehicles via the local ip-interface via API Version 1. In newer chargers the V1 API has to be enabled via the App first. 7 | 8 | ## Features 9 | - attributes from charger available as sensors 10 | - switch to turn off/on charger 11 | - set charge limit in kWh (0.1 kWh steps) 12 | - set max current for charging in ampere (6-32A) 13 | - set absolute maximum current for charging (max can not be set higher than "absolute max") 14 | - no cloud connection needed to control the charger - only local ip-access needed. 15 | - correction factor for older devices which often present 5-10% lower voltage and therefore energy values 16 | 17 | # Warning: WIP - Breaking changes possible 18 | This is the first version of the Integration so there are still breaking changes possible. 19 | 20 | # Installation 21 | 22 | - clone this repository 23 | ``` 24 | git clone https://github.com/cathiele/homeassistant-goecharger.git 25 | ``` 26 | - copy the content of the `custom_components`-Folder to the `custom_components` folder of your home-assistant installation 27 | 28 | ``` 29 | # mkdir -p /custom_components 30 | # cp -r custom_components/goecharger /custom_components 31 | ``` 32 | 33 | * setup your Charger in the `configuration.yaml` (for always connected chargers): 34 | 35 | ```yaml 36 | goecharger: 37 | chargers: 38 | - name: charger1 39 | host: 40 | - name: charger2 41 | host: 42 | correction_factor: factor for correction for total and session charged 43 | ``` 44 | 45 | # Sample View 46 | ![screenshot of Home Assistant](doc/ha_entity_view.png) 47 | 48 | # Example Config 49 | 50 | ## `configuration.yaml` 51 | 52 | ```yaml 53 | input_number: 54 | goecharger_charge_limit: 55 | name: Charge limit (kWh) 56 | min: 0 57 | max: 10 58 | step: 1 59 | 60 | input_select: 61 | goecharger_max_current: 62 | name: Max current 63 | options: 64 | - 6 65 | - 10 66 | - 16 67 | - 20 68 | - 24 69 | - 32 70 | ``` 71 | 72 | ## `automations.yaml` 73 | 74 | **Important: Replace `111111` with your chargers name.** 75 | 76 | ```yaml 77 | - id: '1576914483212' 78 | alias: 'goecharger: set max current on charger based on input select' 79 | description: '' 80 | trigger: 81 | - entity_id: input_select.goecharger_max_current 82 | platform: state 83 | condition: [] 84 | action: 85 | - data_template: 86 | max_current: '{{ states(''input_select.goecharger_max_current'') }}' 87 | service: goecharger.set_max_current 88 | - id: '1576915266692' 89 | alias: 'goecharger: set max_current input_select based on charger value' 90 | description: '' 91 | trigger: 92 | - entity_id: sensor.goecharger_111111_charger_max_current 93 | platform: state 94 | condition: [] 95 | action: 96 | - data_template: 97 | entity_id: input_select.goecharger_max_current 98 | option: '{{ states.sensor.goecharger_111111_charger_max_current.state }}' 99 | service: input_select.select_option 100 | - id: '1577036409850' 101 | alias: 'goecharger: set charge limit based on input' 102 | description: '' 103 | trigger: 104 | - entity_id: input_number.goecharger_charge_limit 105 | platform: state 106 | condition: [] 107 | action: 108 | - data_template: 109 | charge_limit: '{{ states(''input_number.goecharger_charge_limit'') }}' 110 | service: goecharger.set_charge_limit 111 | - id: '1577036687192' 112 | alias: 'goecharger: set charge_limit input based on charger' 113 | description: '' 114 | trigger: 115 | - entity_id: sensor.goecharger_111111_charge_limit 116 | platform: state 117 | condition: [] 118 | action: 119 | - data_template: 120 | entity_id: input_number.goecharger_charge_limit 121 | value: '{{ states.sensor.goecharger_111111_charge_limit.state }}' 122 | service: input_number.set_value 123 | ``` 124 | 125 | ## Lovcelace-UI Card Example 126 | 127 | **Important: Replace `111111` with your chargers name.** 128 | 129 | ```yaml 130 | cards: 131 | entities: 132 | - entity: switch.goecharger_111111_allow_charging 133 | - entity: input_number.goecharger_charge_limit 134 | - entity: input_select.goecharger_max_current 135 | - entity: sensor.goecharger_111111_car_status 136 | - entity: sensor.goecharger_111111_charger_temp 137 | - entity: sensor.goecharger_111111_current_session_charged_energy 138 | - entity: sensor.goecharger_111111_current_session_charged_energy_corrected 139 | - entity: sensor.goecharger_111111_p_all 140 | - entity: sensor.goecharger_111111_p_l1 141 | - entity: sensor.goecharger_111111_p_l2 142 | - entity: sensor.goecharger_111111_p_l3 143 | - entity: sensor.goecharger_111111_u_l1 144 | - entity: sensor.goecharger_111111_u_l2 145 | - entity: sensor.goecharger_111111_u_l3 146 | - entity: sensor.goecharger_111111_i_l1 147 | - entity: sensor.goecharger_111111_i_l2 148 | - entity: sensor.goecharger_111111_i_l3 149 | - entity: sensor.goecharger_111111_energy_total 150 | - entity: sensor.goecharger_111111_energy_total_corrected 151 | show_header_toggle: false 152 | title: EV Charger (go-eCharger) 153 | type: entities 154 | ``` 155 | -------------------------------------------------------------------------------- /configuration.yaml: -------------------------------------------------------------------------------- 1 | default_config: 2 | logger: 3 | default: info 4 | logs: 5 | custom_components.goecharger: debug 6 | -------------------------------------------------------------------------------- /custom_components/goecharger/__init__.py: -------------------------------------------------------------------------------- 1 | """go-eCharger integration""" 2 | 3 | import voluptuous as vol 4 | import ipaddress 5 | import logging 6 | from datetime import timedelta 7 | import homeassistant.helpers.config_validation as cv 8 | from homeassistant.const import CONF_HOST, CONF_SCAN_INTERVAL 9 | from homeassistant.core import valid_entity_id 10 | from homeassistant import core 11 | from homeassistant.helpers.discovery import async_load_platform 12 | from homeassistant.helpers.update_coordinator import DataUpdateCoordinator 13 | 14 | from .const import DOMAIN, CONF_SERIAL, CONF_CHARGERS, CONF_CORRECTION_FACTOR, CONF_NAME, CHARGER_API 15 | from goecharger import GoeCharger 16 | 17 | _LOGGER = logging.getLogger(__name__) 18 | 19 | ABSOLUTE_MAX_CURRENT = "charger_absolute_max_current" 20 | SET_CABLE_LOCK_MODE_ATTR = "cable_lock_mode" 21 | SET_ABSOLUTE_MAX_CURRENT_ATTR = "charger_absolute_max_current" 22 | CHARGE_LIMIT = "charge_limit" 23 | SET_MAX_CURRENT_ATTR = "max_current" 24 | CHARGER_NAME_ATTR = "charger_name" 25 | 26 | MIN_UPDATE_INTERVAL = timedelta(seconds=10) 27 | DEFAULT_UPDATE_INTERVAL = timedelta(seconds=20) 28 | 29 | CONFIG_SCHEMA = vol.Schema( 30 | { 31 | DOMAIN: vol.Schema( 32 | { 33 | vol.Optional(CONF_CHARGERS, default=[]): vol.All([ 34 | cv.ensure_list, [ 35 | vol.All({ 36 | vol.Required(CONF_NAME): vol.All(cv.string), 37 | vol.Required(CONF_HOST): vol.All(ipaddress.ip_address, cv.string), 38 | vol.Optional( 39 | CONF_CORRECTION_FACTOR, default="1.0" 40 | ): vol.All(cv.string), 41 | }) 42 | ] 43 | ]), 44 | vol.Optional(CONF_HOST): vol.All(ipaddress.ip_address, cv.string), 45 | vol.Optional(CONF_SERIAL): vol.All(cv.string), 46 | vol.Optional( 47 | CONF_CORRECTION_FACTOR, default="1.0" 48 | ): vol.All(cv.string), 49 | vol.Optional( 50 | CONF_SCAN_INTERVAL, default=DEFAULT_UPDATE_INTERVAL 51 | ): vol.All(cv.time_period, vol.Clamp(min=MIN_UPDATE_INTERVAL)), 52 | } 53 | ) 54 | }, 55 | extra=vol.ALLOW_EXTRA, 56 | ) 57 | 58 | 59 | async def async_setup_entry(hass, config): 60 | _LOGGER.debug("async_Setup_entry") 61 | _LOGGER.debug(repr(config.data)) 62 | 63 | name = config.data[CONF_NAME] 64 | charger = GoeCharger(config.data[CONF_HOST]) 65 | hass.data[DOMAIN]["api"][name] = charger 66 | 67 | await hass.data[DOMAIN]["coordinator"].async_refresh() 68 | 69 | hass.async_create_task( 70 | hass.config_entries.async_forward_entry_setup(config, "sensor") 71 | ) 72 | hass.async_create_task( 73 | hass.config_entries.async_forward_entry_setup(config, "switch") 74 | ) 75 | return True 76 | 77 | 78 | async def async_unload_entry(hass, entry): 79 | _LOGGER.debug(f"Unloading charger '{entry.data[CONF_NAME]}") 80 | hass.data[DOMAIN]["api"].pop(entry.data[CONF_NAME]) 81 | return True 82 | 83 | 84 | class ChargerStateFetcher: 85 | def __init__(self, hass): 86 | self._hass = hass 87 | 88 | async def fetch_states(self): 89 | _LOGGER.debug('Updating status...') 90 | goeChargers = self._hass.data[DOMAIN]["api"] 91 | data = self.coordinator.data if self.coordinator.data else {} 92 | for chargerName in goeChargers.keys(): 93 | _LOGGER.debug(f"update for '{chargerName}'..") 94 | fetchedStatus = await self._hass.async_add_executor_job(goeChargers[chargerName].requestStatus) 95 | if fetchedStatus.get("car_status", "unknown") != "unknown": 96 | data[chargerName] = fetchedStatus 97 | else: 98 | _LOGGER.error(f"Unable to fetch state for Charger {chargerName}") 99 | return data 100 | 101 | 102 | async def async_setup(hass: core.HomeAssistant, config: dict) -> bool: 103 | """Set up go-eCharger platforms and services.""" 104 | 105 | _LOGGER.debug("async_setup") 106 | scan_interval = DEFAULT_UPDATE_INTERVAL 107 | 108 | hass.data[DOMAIN] = {} 109 | chargerApi = {} 110 | chargers = [] 111 | if DOMAIN in config: 112 | scan_interval = config[DOMAIN].get(CONF_SCAN_INTERVAL, DEFAULT_UPDATE_INTERVAL) 113 | 114 | host = config[DOMAIN].get(CONF_HOST, False) 115 | serial = config[DOMAIN].get(CONF_SERIAL, "unknown") 116 | try: 117 | correctionFactor = float(config[DOMAIN].get(CONF_CORRECTION_FACTOR, "1.0")) 118 | except: 119 | _LOGGER.warn("can't convert correctionFactor, using 1.0") 120 | correctionFactor = 1.0 121 | 122 | chargers = config[DOMAIN].get(CONF_CHARGERS, []) 123 | 124 | if host: 125 | if not serial: 126 | goeCharger = GoeCharger(host) 127 | status = goeCharger.requestStatus() 128 | serial = status["serial_number"] 129 | chargers.append([{CONF_NAME: serial, CONF_HOST: host, CONF_CORRECTION_FACTOR: correctionFactor}]) 130 | _LOGGER.debug(repr(chargers)) 131 | 132 | for charger in chargers: 133 | chargerName = charger[0][CONF_NAME] 134 | host = charger[0][CONF_HOST] 135 | _LOGGER.debug(f"charger: '{chargerName}' host: '{host}' ") 136 | 137 | goeCharger = GoeCharger(host) 138 | chargerApi[chargerName] = goeCharger 139 | 140 | hass.data[DOMAIN]["api"] = chargerApi 141 | 142 | chargeStateFecher = ChargerStateFetcher(hass) 143 | 144 | coordinator = DataUpdateCoordinator( 145 | hass, 146 | _LOGGER, 147 | name=DOMAIN, 148 | update_method=chargeStateFecher.fetch_states, 149 | update_interval=scan_interval, 150 | ) 151 | chargeStateFecher.coordinator = coordinator 152 | 153 | hass.data[DOMAIN]["coordinator"] = coordinator 154 | 155 | await coordinator.async_refresh() 156 | 157 | async def async_handle_set_max_current(call): 158 | """Handle the service call to set the absolute max current.""" 159 | chargerNameInput = call.data.get(CHARGER_NAME_ATTR, '') 160 | 161 | maxCurrentInput = call.data.get( 162 | SET_MAX_CURRENT_ATTR, 32 # TODO: dynamic based on chargers absolute_max-setting 163 | ) 164 | maxCurrent = 0 165 | if isinstance(maxCurrentInput, str): 166 | if maxCurrentInput.isnumeric(): 167 | maxCurrent = int(maxCurrentInput) 168 | elif valid_entity_id(maxCurrentInput): 169 | maxCurrent = int(hass.states.get(maxCurrentInput).state) 170 | else: 171 | _LOGGER.error( 172 | "No valid value for '%s': %s", SET_MAX_CURRENT_ATTR, maxCurrent 173 | ) 174 | return 175 | else: 176 | maxCurrent = maxCurrentInput 177 | 178 | if maxCurrent < 6: 179 | maxCurrent = 6 180 | if maxCurrent > 32: 181 | maxCurrent = 32 182 | 183 | if len(chargerNameInput) > 0: 184 | _LOGGER.debug(f"set max_current for charger '{chargerNameInput}' to {maxCurrent}") 185 | try: 186 | await hass.async_add_executor_job(hass.data[DOMAIN]["api"][chargerNameInput].setTmpMaxCurrent, maxCurrent) 187 | except KeyError: 188 | _LOGGER.error(f"Charger with name '{chargerName}' not found!") 189 | 190 | else: 191 | for charger in hass.data[DOMAIN]["api"].keys(): 192 | try: 193 | _LOGGER.debug(f"set max_current for charger '{charger}' to {maxCurrent}") 194 | await hass.async_add_executor_job(hass.data[DOMAIN]["api"][charger].setTmpMaxCurrent, maxCurrent) 195 | except KeyError: 196 | _LOGGER.error(f"Charger with name '{chargerName}' not found!") 197 | 198 | await hass.data[DOMAIN]["coordinator"].async_refresh() 199 | 200 | async def async_handle_set_absolute_max_current(call): 201 | """Handle the service call to set the absolute max current.""" 202 | chargerNameInput = call.data.get(CHARGER_NAME_ATTR, '') 203 | absoluteMaxCurrentInput = call.data.get(SET_ABSOLUTE_MAX_CURRENT_ATTR, 16) 204 | if isinstance(absoluteMaxCurrentInput, str): 205 | if absoluteMaxCurrentInput.isnumeric(): 206 | absoluteMaxCurrent = int(absoluteMaxCurrentInput) 207 | elif valid_entity_id(absoluteMaxCurrentInput): 208 | absoluteMaxCurrent = int(hass.states.get(absoluteMaxCurrentInput).state) 209 | else: 210 | _LOGGER.error( 211 | "No valid value for '%s': %s", 212 | SET_ABSOLUTE_MAX_CURRENT_ATTR, 213 | absoluteMaxCurrentInput, 214 | ) 215 | return 216 | else: 217 | absoluteMaxCurrent = absoluteMaxCurrentInput 218 | 219 | if absoluteMaxCurrent < 6: 220 | absoluteMaxCurrent = 6 221 | if absoluteMaxCurrent > 32: 222 | absoluteMaxCurrent = 32 223 | 224 | if len(chargerNameInput) > 0: 225 | _LOGGER.debug(f"set absolute_max_current for charger '{chargerNameInput}' to {absoluteMaxCurrent}") 226 | try: 227 | await hass.async_add_executor_job( 228 | hass.data[DOMAIN]["api"][chargerNameInput].setAbsoluteMaxCurrent, absoluteMaxCurrent 229 | ) 230 | except KeyError: 231 | _LOGGER.error(f"Charger with name '{chargerName}' not found!") 232 | 233 | else: 234 | for charger in hass.data[DOMAIN]["api"].keys(): 235 | try: 236 | _LOGGER.debug(f"set absolute_max_current for charger '{charger}' to {absoluteMaxCurrent}") 237 | await hass.async_add_executor_job( 238 | hass.data[DOMAIN]["api"][charger].setAbsoluteMaxCurrent, absoluteMaxCurrent 239 | ) 240 | except KeyError: 241 | _LOGGER.error(f"Charger with name '{chargerName}' not found!") 242 | 243 | await hass.data[DOMAIN]["coordinator"].async_refresh() 244 | 245 | async def async_handle_set_cable_lock_mode(call): 246 | """Handle the service call to set the absolute max current.""" 247 | chargerNameInput = call.data.get(CHARGER_NAME_ATTR, '') 248 | cableLockModeInput = call.data.get(SET_CABLE_LOCK_MODE_ATTR, 0) 249 | if isinstance(cableLockModeInput, str): 250 | if cableLockModeInput.isnumeric(): 251 | cableLockMode = int(cableLockModeInput) 252 | elif valid_entity_id(cableLockModeInput): 253 | cableLockMode = int(hass.states.get(cableLockModeInput).state) 254 | else: 255 | _LOGGER.error( 256 | "No valid value for '%s': %s", 257 | SET_CABLE_LOCK_MODE_ATTR, 258 | cableLockModeInput, 259 | ) 260 | return 261 | else: 262 | cableLockMode = cableLockModeInput 263 | 264 | cableLockModeEnum = GoeCharger.CableLockMode.UNLOCKCARFIRST 265 | if cableLockModeInput == 1: 266 | cableLockModeEnum = GoeCharger.CableLockMode.AUTOMATIC 267 | if cableLockMode >= 2: 268 | cableLockModeEnum = GoeCharger.CableLockMode.LOCKED 269 | 270 | if len(chargerNameInput) > 0: 271 | _LOGGER.debug(f"set set_cable_lock_mode for charger '{chargerNameInput}' to {cableLockModeEnum}") 272 | try: 273 | await hass.async_add_executor_job( 274 | hass.data[DOMAIN]["api"][chargerNameInput].setCableLockMode, cableLockModeEnum 275 | ) 276 | except KeyError: 277 | _LOGGER.error(f"Charger with name '{chargerName}' not found!") 278 | 279 | else: 280 | for charger in hass.data[DOMAIN]["api"].keys(): 281 | try: 282 | _LOGGER.debug(f"set set_cable_lock_mode for charger '{charger}' to {cableLockModeEnum}") 283 | await hass.async_add_executor_job(hass.data[DOMAIN]["api"][charger].setCableLockMode, cableLockModeEnum) 284 | except KeyError: 285 | _LOGGER.error(f"Charger with name '{chargerName}' not found!") 286 | 287 | await hass.data[DOMAIN]["coordinator"].async_refresh() 288 | 289 | async def async_handle_set_charge_limit(call): 290 | """Handle the service call to set charge limit.""" 291 | chargerNameInput = call.data.get(CHARGER_NAME_ATTR, '') 292 | chargeLimitInput = call.data.get(CHARGE_LIMIT, 0.0) 293 | if isinstance(chargeLimitInput, str): 294 | if chargeLimitInput.isnumeric(): 295 | chargeLimit = float(chargeLimitInput) 296 | elif valid_entity_id(chargeLimitInput): 297 | chargeLimit = float(hass.states.get(chargeLimitInput).state) 298 | else: 299 | _LOGGER.error( 300 | "No valid value for '%s': %s", CHARGE_LIMIT, chargeLimitInput 301 | ) 302 | return 303 | else: 304 | chargeLimit = chargeLimitInput 305 | 306 | if chargeLimit < 0: 307 | chargeLimit = 0 308 | 309 | if len(chargerNameInput) > 0: 310 | _LOGGER.debug(f"set set_charge_limit for charger '{chargerNameInput}' to {chargeLimit}") 311 | try: 312 | await hass.async_add_executor_job(hass.data[DOMAIN]["api"][chargerNameInput].setChargeLimit, chargeLimit) 313 | except KeyError: 314 | _LOGGER.error(f"Charger with name '{chargerName}' not found!") 315 | 316 | else: 317 | for charger in hass.data[DOMAIN]["api"].keys(): 318 | try: 319 | _LOGGER.debug(f"set set_charge_limit for charger '{charger}' to {chargeLimit}") 320 | await hass.async_add_executor_job(hass.data[DOMAIN]["api"][charger].setChargeLimit, chargeLimit) 321 | except KeyError: 322 | _LOGGER.error(f"Charger with name '{chargerName}' not found!") 323 | 324 | await hass.data[DOMAIN]["coordinator"].async_refresh() 325 | 326 | hass.services.async_register(DOMAIN, "set_max_current", async_handle_set_max_current) 327 | hass.services.async_register( 328 | DOMAIN, "set_absolute_max_current", async_handle_set_absolute_max_current 329 | ) 330 | hass.services.async_register(DOMAIN, "set_cable_lock_mode", async_handle_set_cable_lock_mode) 331 | hass.services.async_register(DOMAIN, "set_charge_limit", async_handle_set_charge_limit) 332 | 333 | hass.async_create_task(async_load_platform( 334 | hass, "sensor", DOMAIN, {CONF_CHARGERS: chargers, CHARGER_API: chargerApi}, config) 335 | ) 336 | hass.async_create_task(async_load_platform( 337 | hass, "switch", DOMAIN, {CONF_CHARGERS: chargers, CHARGER_API: chargerApi}, config) 338 | ) 339 | 340 | return True 341 | -------------------------------------------------------------------------------- /custom_components/goecharger/config_flow.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from datetime import timedelta 3 | import voluptuous as vol 4 | 5 | from homeassistant import config_entries 6 | from homeassistant.core import callback 7 | 8 | from homeassistant.const import CONF_HOST, CONF_SCAN_INTERVAL 9 | from .const import DOMAIN, CONF_NAME, CONF_CORRECTION_FACTOR 10 | _LOGGER = logging.getLogger(__name__) 11 | 12 | 13 | DEFAULT_UPDATE_INTERVAL = timedelta(seconds=20) 14 | MIN_UPDATE_INTERVAL = timedelta(seconds=10) 15 | 16 | 17 | class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): 18 | """Config flow for go-eCharger setup.""" 19 | VERSION = 1 20 | 21 | @staticmethod 22 | @callback 23 | async def async_get_options_flow(config_entry): 24 | return OptionsFlowHandler(config_entry) 25 | 26 | async def async_step_user(self, info): 27 | if info is not None: 28 | _LOGGER.debug(info) 29 | return self.async_create_entry(title=info[CONF_NAME], data=info) 30 | 31 | return self.async_show_form( 32 | step_id="user", data_schema=vol.Schema( 33 | { 34 | vol.Required(CONF_HOST): str, 35 | vol.Required(CONF_NAME): str, 36 | vol.Optional( 37 | CONF_SCAN_INTERVAL, default=20 38 | ): int, 39 | vol.Required( 40 | CONF_CORRECTION_FACTOR, default="1.0" 41 | ): str, 42 | } 43 | ), 44 | ) 45 | -------------------------------------------------------------------------------- /custom_components/goecharger/const.py: -------------------------------------------------------------------------------- 1 | DOMAIN = "goecharger" 2 | CONF_NAME = "name" 3 | CONF_SERIAL = "serial" 4 | CONF_CHARGERS = "chargers" 5 | CONF_CORRECTION_FACTOR = "correction_factor" 6 | CHARGER_API = "charger_api" 7 | -------------------------------------------------------------------------------- /custom_components/goecharger/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "goecharger", 3 | "name": "go-eCharger", 4 | "codeowners": [ 5 | "@cathiele" 6 | ], 7 | "config_flow": true, 8 | "dependencies": [], 9 | "documentation": "https://github.com/cathiele/homeassistant-goecharger", 10 | "iot_class": "local_polling", 11 | "issue_tracker": "https://github.com/cathiele/homeassistant-goecharger/issues", 12 | "requirements": [ 13 | "goecharger==0.0.16" 14 | ], 15 | "version": "0.26.0" 16 | } 17 | -------------------------------------------------------------------------------- /custom_components/goecharger/sensor.py: -------------------------------------------------------------------------------- 1 | """Platform for go-eCharger sensor integration.""" 2 | import logging 3 | from homeassistant.const import ( 4 | UnitOfTemperature, 5 | UnitOfEnergy 6 | ) 7 | 8 | from homeassistant import core, config_entries 9 | from homeassistant.helpers.update_coordinator import CoordinatorEntity 10 | from homeassistant.components.sensor import ( 11 | SensorStateClass, 12 | SensorDeviceClass, 13 | SensorEntity 14 | ) 15 | 16 | 17 | from .const import CONF_CHARGERS, DOMAIN, CONF_NAME, CONF_CORRECTION_FACTOR 18 | 19 | AMPERE = 'A' 20 | VOLT = 'V' 21 | UnitOfEnergy.KILO_WATT = 'kW' 22 | CARD_ID = 'Card ID' 23 | PERCENT = '%' 24 | 25 | _LOGGER = logging.getLogger(__name__) 26 | 27 | _sensorUnits = { 28 | 'charger_temp': {'unit': UnitOfTemperature.CELSIUS, 'name': 'Charger Temp'}, 29 | 'charger_temp0': {'unit': UnitOfTemperature.CELSIUS, 'name': 'Charger Temp 0'}, 30 | 'charger_temp1': {'unit': UnitOfTemperature.CELSIUS, 'name': 'Charger Temp 1'}, 31 | 'charger_temp2': {'unit': UnitOfTemperature.CELSIUS, 'name': 'Charger Temp 2'}, 32 | 'charger_temp3': {'unit': UnitOfTemperature.CELSIUS, 'name': 'Charger Temp 3'}, 33 | 'p_l1': {'unit': UnitOfEnergy.KILO_WATT, 'name': 'Power L1'}, 34 | 'p_l2': {'unit': UnitOfEnergy.KILO_WATT, 'name': 'Power L2'}, 35 | 'p_l3': {'unit': UnitOfEnergy.KILO_WATT, 'name': 'Power L3'}, 36 | 'p_n': {'unit': UnitOfEnergy.KILO_WATT, 'name': 'Power N'}, 37 | 'p_all': {'unit': UnitOfEnergy.KILO_WATT, 'name': 'Power All'}, 38 | 'current_session_charged_energy': {'unit': UnitOfEnergy.KILO_WATT_HOUR, 'name': 'Current Session charged'}, 39 | 'current_session_charged_energy_corrected': {'unit': UnitOfEnergy.KILO_WATT_HOUR, 'name': 'Current Session charged corrected'}, 40 | 'energy_total': {'unit': UnitOfEnergy.KILO_WATT_HOUR, 'name': 'Total Charged'}, 41 | 'energy_total_corrected': {'unit': UnitOfEnergy.KILO_WATT_HOUR, 'name': 'Total Charged corrected'}, 42 | 'charge_limit': {'unit': UnitOfEnergy.KILO_WATT_HOUR, 'name': 'Charge limit'}, 43 | 'u_l1': {'unit': VOLT, 'name': 'Voltage L1'}, 44 | 'u_l2': {'unit': VOLT, 'name': 'Voltage L2'}, 45 | 'u_l3': {'unit': VOLT, 'name': 'Voltage L3'}, 46 | 'u_n': {'unit': VOLT, 'name': 'Voltage N'}, 47 | 'i_l1': {'unit': AMPERE, 'name': 'Current L1'}, 48 | 'i_l2': {'unit': AMPERE, 'name': 'Current L2'}, 49 | 'i_l3': {'unit': AMPERE, 'name': 'Current L3'}, 50 | 'charger_max_current': {'unit': AMPERE, 'name': 'Charger max current setting'}, 51 | 'charger_absolute_max_current': {'unit': AMPERE, 'name': 'Charger absolute max current setting'}, 52 | 'cable_lock_mode': {'unit': '', 'name': 'Cable lock mode'}, 53 | 'cable_max_current': {'unit': AMPERE, 'name': 'Cable max current'}, 54 | 'unlocked_by_card': {'unit': CARD_ID, 'name': 'Card used'}, 55 | 'lf_l1': {'unit': PERCENT, 'name': 'Power factor L1'}, 56 | 'lf_l2': {'unit': PERCENT, 'name': 'Power factor L2'}, 57 | 'lf_l3': {'unit': PERCENT, 'name': 'Power factor L3'}, 58 | 'lf_n': {'unit': PERCENT, 'name': 'Loadfactor N'}, 59 | 'car_status': {'unit': '', 'name': 'Status'} 60 | } 61 | 62 | _sensorStateClass = { 63 | 'energy_total': SensorStateClass.TOTAL_INCREASING, 64 | 'energy_total_corrected': SensorStateClass.TOTAL_INCREASING, 65 | 'current_session_charged_energy': SensorStateClass.TOTAL_INCREASING, 66 | 'current_session_charged_energy_corrected': SensorStateClass.TOTAL_INCREASING 67 | } 68 | 69 | _sensorDeviceClass = { 70 | 'energy_total': SensorDeviceClass.ENERGY, 71 | 'energy_total_corrected': SensorDeviceClass.ENERGY, 72 | 'current_session_charged_energy': SensorDeviceClass.ENERGY, 73 | 'current_session_charged_energy_corrected': SensorDeviceClass.ENERGY 74 | } 75 | 76 | _sensors = [ 77 | 'car_status', 78 | 'charger_max_current', 79 | 'charger_absolute_max_current', 80 | 'charger_err', 81 | 'charger_access', 82 | 'stop_mode', 83 | 'cable_lock_mode', 84 | 'cable_max_current', 85 | 'pre_contactor_l1', 86 | 'pre_contactor_l2', 87 | 'pre_contactor_l3', 88 | 'post_contactor_l1', 89 | 'post_contactor_l2', 90 | 'post_contactor_l3', 91 | 'charger_temp', 92 | 'charger_temp0', 93 | 'charger_temp1', 94 | 'charger_temp2', 95 | 'charger_temp3', 96 | 'current_session_charged_energy', 97 | 'current_session_charged_energy_corrected', 98 | 'charge_limit', 99 | 'adapter', 100 | 'unlocked_by_card', 101 | 'energy_total', 102 | 'energy_total_corrected', 103 | 'wifi', 104 | 105 | 'u_l1', 106 | 'u_l2', 107 | 'u_l3', 108 | 'u_n', 109 | 'i_l1', 110 | 'i_l2', 111 | 'i_l3', 112 | 'p_l1', 113 | 'p_l2', 114 | 'p_l3', 115 | 'p_n', 116 | 'p_all', 117 | 'lf_l1', 118 | 'lf_l2', 119 | 'lf_l3', 120 | 'lf_n', 121 | 122 | 'firmware', 123 | 'serial_number', 124 | 'wifi_ssid', 125 | 'wifi_enabled', 126 | 'timezone_offset', 127 | 'timezone_dst_offset', 128 | ] 129 | 130 | 131 | def _create_sensors_for_charger(chargerName, hass, correctionFactor): 132 | entities = [] 133 | 134 | for sensor in _sensors: 135 | 136 | _LOGGER.debug(f"adding Sensor: {sensor} for charger {chargerName}") 137 | sensorUnit = _sensorUnits.get(sensor).get('unit') if _sensorUnits.get(sensor) else '' 138 | sensorName = _sensorUnits.get(sensor).get('name') if _sensorUnits.get(sensor) else sensor 139 | sensorStateClass = _sensorStateClass[sensor] if sensor in _sensorStateClass else '' 140 | sensorDeviceClass = _sensorDeviceClass[sensor] if sensor in _sensorDeviceClass else '' 141 | entities.append( 142 | GoeChargerSensor( 143 | hass.data[DOMAIN]["coordinator"], 144 | f"sensor.goecharger_{chargerName}_{sensor}", 145 | chargerName, sensorName, sensor, sensorUnit, sensorStateClass, sensorDeviceClass, correctionFactor 146 | ) 147 | ) 148 | 149 | return entities 150 | 151 | 152 | async def async_setup_entry( 153 | hass: core.HomeAssistant, 154 | config_entry: config_entries.ConfigEntry, 155 | async_add_entities, 156 | ): 157 | _LOGGER.debug("setup sensors...") 158 | config = config_entry.as_dict()["data"] 159 | 160 | chargerName = config[CONF_NAME] 161 | 162 | correctionFactor = 1.0 163 | if CONF_CORRECTION_FACTOR in config: 164 | try: 165 | correctionFactor = float(config[CONF_CORRECTION_FACTOR]) 166 | except: 167 | correctionFactor = 1.0 168 | 169 | _LOGGER.debug(f"charger name: '{chargerName}'") 170 | _LOGGER.debug(f"config: '{config}'") 171 | async_add_entities(_create_sensors_for_charger(chargerName, hass, correctionFactor)) 172 | 173 | 174 | async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): 175 | """Set up go-eCharger Sensor platform.""" 176 | _LOGGER.debug("setup_platform") 177 | if discovery_info is None: 178 | return 179 | 180 | chargers = discovery_info[CONF_CHARGERS] 181 | 182 | entities = [] 183 | for charger in chargers: 184 | chargerName = charger[0][CONF_NAME] 185 | _LOGGER.debug(f"charger name: '{chargerName}'") 186 | _LOGGER.debug(f"charger[0]: '{charger[0]}'") 187 | correctionFactor = 1.0 188 | if CONF_CORRECTION_FACTOR in charger[0]: 189 | try: 190 | correctionFactor = charger[0][CONF_CORRECTION_FACTOR] 191 | except: 192 | __LOGGER.warn(f"can't parse correctionFactor. Using 1.0") 193 | correctionFactor = 1.0 194 | 195 | entities.extend(_create_sensors_for_charger(chargerName, hass, correctionFactor)) 196 | 197 | async_add_entities(entities) 198 | 199 | 200 | class GoeChargerSensor(CoordinatorEntity, SensorEntity): 201 | def __init__(self, coordinator, entity_id, chargerName, name, attribute, unit, stateClass, deviceClass, correctionFactor): 202 | """Initialize the go-eCharger sensor.""" 203 | 204 | super().__init__(coordinator) 205 | self._chargername = chargerName 206 | self.entity_id = entity_id 207 | self._name = name 208 | self._attribute = attribute 209 | self._unit = unit 210 | self._attr_state_class = stateClass 211 | self._attr_device_class = deviceClass 212 | self.correctionFactor = correctionFactor 213 | 214 | 215 | @property 216 | def device_info(self): 217 | return { 218 | "identifiers": { 219 | # Serial numbers are unique identifiers within a specific domain 220 | (DOMAIN, self._chargername) 221 | }, 222 | "name": self._chargername, 223 | "manufacturer": "go-e", 224 | "model": "HOME", 225 | } 226 | 227 | @property 228 | def name(self): 229 | """Return the name of the sensor.""" 230 | return self._name 231 | 232 | @property 233 | def unique_id(self): 234 | """Return the unique_id of the sensor.""" 235 | return f"{self._chargername}_{self._attribute}" 236 | 237 | @property 238 | def state(self): 239 | """Return the state of the sensor.""" 240 | if (self._attribute == 'energy_total_corrected'): 241 | return self.coordinator.data[self._chargername]['energy_total'] * self.correctionFactor 242 | if (self._attribute == 'current_session_charged_energy_corrected'): 243 | return self.coordinator.data[self._chargername]['current_session_charged_energy'] * self.correctionFactor 244 | return self.coordinator.data[self._chargername][self._attribute] 245 | 246 | @property 247 | def unit_of_measurement(self): 248 | """Return the unit of measurement.""" 249 | return self._unit 250 | -------------------------------------------------------------------------------- /custom_components/goecharger/services.yaml: -------------------------------------------------------------------------------- 1 | set_max_current: 2 | fields: 3 | charger_name: 4 | example: "charger1" 5 | max_current: 6 | example: "16" 7 | set_cable_lock_mode: 8 | fields: 9 | charger_name: 10 | example: "charger1" 11 | cable_lock_mode: 12 | example: "0" 13 | set_absolute_max_current: 14 | fields: 15 | charger_name: 16 | example: "charger1" 17 | charger_absolute_max_current: 18 | example: "16" 19 | set_charge_limit: 20 | fields: 21 | charger_name: 22 | example: "charger1" 23 | charge_limit: 24 | example: "2.5" 25 | -------------------------------------------------------------------------------- /custom_components/goecharger/switch.py: -------------------------------------------------------------------------------- 1 | """Platform for go-eCharger switch integration.""" 2 | import logging 3 | from homeassistant.components.switch import SwitchEntity 4 | from homeassistant.const import CONF_HOST 5 | from homeassistant.helpers.update_coordinator import CoordinatorEntity 6 | from homeassistant import core, config_entries 7 | 8 | from goecharger import GoeCharger 9 | 10 | from .const import DOMAIN, CONF_CHARGERS, CONF_NAME, CHARGER_API 11 | 12 | _LOGGER = logging.getLogger(__name__) 13 | 14 | 15 | async def async_setup_entry( 16 | hass: core.HomeAssistant, 17 | config_entry: config_entries.ConfigEntry, 18 | async_add_entities, 19 | ): 20 | _LOGGER.debug("setup switch...") 21 | _LOGGER.debug(repr(config_entry.as_dict())) 22 | config = config_entry.as_dict()["data"] 23 | 24 | chargerName = config[CONF_NAME] 25 | host = config[CONF_HOST] 26 | chargerApi = GoeCharger(host) 27 | 28 | entities = [] 29 | 30 | attribute = "allow_charging" 31 | entities.append( 32 | GoeChargerSwitch( 33 | hass.data[DOMAIN]["coordinator"], 34 | hass, 35 | chargerApi, 36 | f"switch.goecharger_{chargerName}_{attribute}", 37 | chargerName, 38 | "Charging allowed", 39 | attribute, 40 | ) 41 | ) 42 | 43 | async_add_entities(entities) 44 | 45 | 46 | async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): 47 | """Set up go-eCharger Switch platform.""" 48 | if discovery_info is None: 49 | return 50 | _LOGGER.debug("setup_platform") 51 | 52 | chargers = discovery_info[CONF_CHARGERS] 53 | chargerApi = discovery_info[CHARGER_API] 54 | 55 | entities = [] 56 | 57 | for charger in chargers: 58 | chargerName = charger[0][CONF_NAME] 59 | 60 | attribute = "allow_charging" 61 | entities.append( 62 | GoeChargerSwitch( 63 | hass.data[DOMAIN]["coordinator"], 64 | hass, 65 | chargerApi[chargerName], 66 | f"switch.goecharger_{chargerName}_{attribute}", 67 | chargerName, 68 | "Charging allowed", 69 | attribute, 70 | ) 71 | ) 72 | async_add_entities(entities) 73 | 74 | 75 | class GoeChargerSwitch(CoordinatorEntity, SwitchEntity): 76 | def __init__(self, coordinator, hass, goeCharger, entity_id, chargerName, name, attribute): 77 | """Initialize the go-eCharger switch.""" 78 | super().__init__(coordinator) 79 | self.entity_id = entity_id 80 | self._chargername = chargerName 81 | self._name = name 82 | self._attribute = attribute 83 | self.hass = hass 84 | self._goeCharger = goeCharger 85 | self._state = None 86 | 87 | @property 88 | def device_info(self): 89 | return { 90 | "identifiers": { 91 | # Serial numbers are unique identifiers within a specific domain 92 | (DOMAIN, self._chargername) 93 | }, 94 | "name": self.name, 95 | "manufacturer": "go-e", 96 | "model": "HOME", 97 | } 98 | 99 | async def async_turn_on(self, **kwargs): 100 | """Turn the entity on.""" 101 | await self.hass.async_add_executor_job(self._goeCharger.setAllowCharging, True) 102 | await self.coordinator.async_request_refresh() 103 | 104 | async def async_turn_off(self, **kwargs): 105 | """Turn the entity off.""" 106 | await self.hass.async_add_executor_job(self._goeCharger.setAllowCharging, False) 107 | await self.coordinator.async_request_refresh() 108 | 109 | @property 110 | def name(self): 111 | """Return the name of the switch.""" 112 | return self._chargername 113 | 114 | @property 115 | def unique_id(self): 116 | """Return the unique_id of the switch.""" 117 | return f"{self._chargername}_{self._attribute}" 118 | 119 | @property 120 | def is_on(self): 121 | """Return the state of the switch.""" 122 | return True if self.coordinator.data[self._chargername][self._attribute] == "on" else False 123 | -------------------------------------------------------------------------------- /custom_components/goecharger/translations/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "title": "go-eCharger hinzufügen", 4 | "step": { 5 | "user": { 6 | "title": "go-eCharger Setup", 7 | "description": "Ein Liste möglicher Konfigurationsparameter findest du hier : https://github.com/cathiele/homeassistant-goecharger", 8 | "data": { 9 | "host": "Hostname oder IP-Address im lokalen Netzwerk", 10 | "name": "Im Homeassistant eindeutiger Name des Chargers", 11 | "scan_interval": "Abfrageintervall in Sekunden", 12 | "correction_factor": "Korrekturfaktor für ungenaue Spannungsmessung" 13 | } 14 | } 15 | }, 16 | "abort": { 17 | } 18 | }, 19 | "services": { 20 | "set_max_current": { 21 | "name": "Setze maximalen Strom", 22 | "description": "Setzt den maximalen Ladestrom in Ampere", 23 | "fields": { 24 | "charger_name": { 25 | "name": "Ladername", 26 | "description": "Name des zu ändernden Chargers (Wenn kein name angegeben wird werden alle geändert)" 27 | }, 28 | "max_current": { 29 | "name": "Maximaler Strom", 30 | "description": "maximaler Strom in Ampere (6-32)" 31 | } 32 | } 33 | }, 34 | "set_cable_lock_mode": { 35 | "name": "Setze Kabellock-Modus", 36 | "description": "Setzt das Verhalten des Ladekabels", 37 | "fields": { 38 | "charger_name": { 39 | "name": "Ladername", 40 | "description": "Name des zu ändernden Chargers (Wenn kein name angegeben wird werden alle geändert)" 41 | }, 42 | "cable_lock_mode": { 43 | "name": "Gewünschter Modus", 44 | "description": "Gewünschtes Verhalten (0=verriegelt wenn Auto verbunden, 1=Entriegeln wenn Ladevorgang beendet, 2=immer verriegelt)" 45 | } 46 | } 47 | }, 48 | "set_absolute_max_current": { 49 | "name": "Setze absolut maximalen ladestrom", 50 | "description": "Setzt den Wert des absoluten maximalen Ladestroms des Laders in Ampere", 51 | "fields": { 52 | "charger_name": { 53 | "name": "Ladername", 54 | "description": "Name des zu ändernden Chargers (Wenn kein name angegeben wird werden alle geändert)" 55 | }, 56 | "charger_absolute_max_current": { 57 | "name": "Absolut maximaler Ladestrom", 58 | "description": "Absolut maximaler Ladestrom in Ampere (6-32)" 59 | } 60 | } 61 | }, 62 | "set_charge_limit": { 63 | "name": "Setze Ladelimit", 64 | "description": "Setzt das Ladelimit in kWh", 65 | "fields": { 66 | "charger_name": { 67 | "name": "Ladername", 68 | "description": "Name des zu ändernden Chargers (Wenn kein name angegeben wird werden alle geändert)" 69 | }, 70 | "charge_limit": { 71 | "name": "Ladelimit", 72 | "description": "Ladelimit in kWh, z.B. '2.5'" 73 | } 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /custom_components/goecharger/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "title": "Add go-eCharger", 4 | "step": { 5 | "user": { 6 | "title": "go-eCharger Setup", 7 | "description": "If you need help with the configuration have a look here: https://github.com/cathiele/homeassistant-goecharger", 8 | "data": { 9 | "host": "Hostname or IP-Address in the local network", 10 | "name": "uniq name of the charger", 11 | "scan_interval": "Pollinterval in seconds", 12 | "correction_factor": "correction Factor for incorrect Voltage measurement" 13 | } 14 | } 15 | }, 16 | "abort": { 17 | } 18 | }, 19 | "services": { 20 | "set_max_current": { 21 | "name": "Set max current", 22 | "description": "Sets the maximum current in Ampere of the Charger.", 23 | "fields": { 24 | "charger_name": { 25 | "name": "Charger name", 26 | "description": "name of the charger to update (if not specified all chargers will be changed)" 27 | }, 28 | "max_current": { 29 | "name": "Maximum current", 30 | "description": "current to be set (6-32)" 31 | } 32 | } 33 | }, 34 | "set_cable_lock_mode": { 35 | "name": "Set cable lock mode", 36 | "description": "Sets the cable lock mode of the Charger.", 37 | "fields": { 38 | "charger_name": { 39 | "name": "Charger name", 40 | "description": "name of the charger to update (if not specified all chargers will be changed)" 41 | }, 42 | "cable_lock_mode": { 43 | "name": "Cable lock mode", 44 | "description": "lock mode for the cable connected to the charger (0=locked while car connected, 1=unlock after charging finished, 2=always locked)" 45 | } 46 | } 47 | }, 48 | "set_absolute_max_current": { 49 | "name": "Set absolute max current", 50 | "description": "Sets the absolute maximum current in Ampere of the Charger.", 51 | "fields": { 52 | "charger_name": { 53 | "name": "Charger name", 54 | "description": "name of the charger to update (if not specified all chargers will be changed)" 55 | }, 56 | "charger_absolute_max_current": { 57 | "name": "absolute max charge current", 58 | "description": "absolute maximum current to be set (6-32)" 59 | } 60 | } 61 | }, 62 | "set_charge_limit": { 63 | "name": "Set charge limit", 64 | "description": "Sets the charge limit in kWh of the Charger.", 65 | "fields": { 66 | "charger_name": { 67 | "name": "Charger name", 68 | "description": "name of the charger to update (if not specified all chargers will be changed)" 69 | }, 70 | "charge_limit": { 71 | "name": "charge limit", 72 | "description": "charge limit in kWh example '2.5'" 73 | } 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /doc/ha_entity_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cathiele/homeassistant-goecharger/695ed2f60c99233d16b3b570f70849101c0f4c57/doc/ha_entity_view.png -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.2" 2 | services: 3 | hass: 4 | image: homeassistant/home-assistant:2025.1.0 5 | volumes: 6 | - ./custom_components:/config/custom_components:rw 7 | - ./configuration.yaml:/config/configuration.yaml:rw 8 | ports: 9 | - 8123:8123 10 | -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "go-eCharger", 3 | "content_in_root": false, 4 | "render_readme": true, 5 | "domains": ["switch", "sensor"] 6 | } 7 | --------------------------------------------------------------------------------