├── custom_components ├── mpu650 │ ├── mpu6050_calibration.json │ ├── __init__.py │ ├── manifest.json │ ├── button.py │ ├── switch.py │ ├── mpuAngle.py │ └── sensor.py ├── mcp23017 │ ├── .DS_Store │ ├── manifest.json │ ├── const.py │ ├── strings.json │ ├── translations │ │ └── en.json │ ├── config_flow.py │ ├── switch.py │ ├── binary_sensor.py │ └── __init__.py ├── onewire_sysbus │ ├── .DS_Store │ ├── manifest.json │ ├── model.py │ ├── strings.json │ ├── translations │ │ ├── en.json │ │ ├── sk.json │ │ └── pt-BR.json │ ├── const.py │ ├── diagnostics.py │ ├── __init__.py │ ├── onewire_entities.py │ ├── config_flow.py │ ├── onewirehub.py │ └── sensor.py ├── victron_mppt │ ├── __init__.py │ ├── manifest.json │ ├── config.py │ ├── test.py │ └── sensor.py ├── victron_smartshunt │ ├── __init__.py │ ├── manifest.json │ ├── config.py │ └── sensor.py └── ads_waterlevel │ ├── __init__.py │ ├── manifest.json │ └── sensor.py ├── assets ├── Dateien.png ├── preview.png ├── Bildschirmfoto 2024-11-04 um 21.38.08.png ├── Bildschirmfoto 2024-11-04 um 21.42.28.png ├── Bildschirmfoto 2024-11-04 um 21.55.13.png ├── Bildschirmfoto 2024-11-04 um 22.21.54.png ├── Bildschirmfoto 2024-11-05 um 07.14.15.png ├── Bildschirmfoto 2024-11-05 um 07.16.25.png ├── Bildschirmfoto 2024-11-05 um 20.42.50.png ├── Bildschirmfoto 2024-11-05 um 20.43.00.png ├── Bildschirmfoto 2024-11-05 um 20.43.10.png ├── Bildschirmfoto 2024-11-05 um 20.52.51.png ├── Bildschirmfoto 2024-11-06 um 00.08.07.png ├── 377777963-13886d0d-30d3-4d62-821f-5db632fde90d.png ├── 377779266-6ce55e0e-9ff7-4df2-9c88-53bf6e7bb89c.png └── 377796060-22ff2178-28d7-4c6b-86b3-a63f1ed0face.png ├── .github └── workflows │ └── hassfest.yaml ├── hacs.json ├── LICENSE ├── installHA.md ├── README.md ├── .gitignore └── info.md /custom_components/mpu650/mpu6050_calibration.json: -------------------------------------------------------------------------------- 1 | {"x_offset": 1127.64, "y_offset": -527.3333333333334} -------------------------------------------------------------------------------- /assets/Dateien.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxlin1/PekawayVANPICORE_homeassistant/HEAD/assets/Dateien.png -------------------------------------------------------------------------------- /assets/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxlin1/PekawayVANPICORE_homeassistant/HEAD/assets/preview.png -------------------------------------------------------------------------------- /custom_components/mcp23017/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxlin1/PekawayVANPICORE_homeassistant/HEAD/custom_components/mcp23017/.DS_Store -------------------------------------------------------------------------------- /custom_components/onewire_sysbus/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxlin1/PekawayVANPICORE_homeassistant/HEAD/custom_components/onewire_sysbus/.DS_Store -------------------------------------------------------------------------------- /assets/Bildschirmfoto 2024-11-04 um 21.38.08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxlin1/PekawayVANPICORE_homeassistant/HEAD/assets/Bildschirmfoto 2024-11-04 um 21.38.08.png -------------------------------------------------------------------------------- /assets/Bildschirmfoto 2024-11-04 um 21.42.28.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxlin1/PekawayVANPICORE_homeassistant/HEAD/assets/Bildschirmfoto 2024-11-04 um 21.42.28.png -------------------------------------------------------------------------------- /assets/Bildschirmfoto 2024-11-04 um 21.55.13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxlin1/PekawayVANPICORE_homeassistant/HEAD/assets/Bildschirmfoto 2024-11-04 um 21.55.13.png -------------------------------------------------------------------------------- /assets/Bildschirmfoto 2024-11-04 um 22.21.54.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxlin1/PekawayVANPICORE_homeassistant/HEAD/assets/Bildschirmfoto 2024-11-04 um 22.21.54.png -------------------------------------------------------------------------------- /assets/Bildschirmfoto 2024-11-05 um 07.14.15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxlin1/PekawayVANPICORE_homeassistant/HEAD/assets/Bildschirmfoto 2024-11-05 um 07.14.15.png -------------------------------------------------------------------------------- /assets/Bildschirmfoto 2024-11-05 um 07.16.25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxlin1/PekawayVANPICORE_homeassistant/HEAD/assets/Bildschirmfoto 2024-11-05 um 07.16.25.png -------------------------------------------------------------------------------- /assets/Bildschirmfoto 2024-11-05 um 20.42.50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxlin1/PekawayVANPICORE_homeassistant/HEAD/assets/Bildschirmfoto 2024-11-05 um 20.42.50.png -------------------------------------------------------------------------------- /assets/Bildschirmfoto 2024-11-05 um 20.43.00.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxlin1/PekawayVANPICORE_homeassistant/HEAD/assets/Bildschirmfoto 2024-11-05 um 20.43.00.png -------------------------------------------------------------------------------- /assets/Bildschirmfoto 2024-11-05 um 20.43.10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxlin1/PekawayVANPICORE_homeassistant/HEAD/assets/Bildschirmfoto 2024-11-05 um 20.43.10.png -------------------------------------------------------------------------------- /assets/Bildschirmfoto 2024-11-05 um 20.52.51.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxlin1/PekawayVANPICORE_homeassistant/HEAD/assets/Bildschirmfoto 2024-11-05 um 20.52.51.png -------------------------------------------------------------------------------- /assets/Bildschirmfoto 2024-11-06 um 00.08.07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxlin1/PekawayVANPICORE_homeassistant/HEAD/assets/Bildschirmfoto 2024-11-06 um 00.08.07.png -------------------------------------------------------------------------------- /custom_components/victron_mppt/__init__.py: -------------------------------------------------------------------------------- 1 | # __init__.py 2 | 3 | async def async_setup(hass, config): 4 | """Set up the Victron component.""" 5 | return True 6 | -------------------------------------------------------------------------------- /custom_components/victron_smartshunt/__init__.py: -------------------------------------------------------------------------------- 1 | # __init__.py 2 | 3 | async def async_setup(hass, config): 4 | """Set up the Smartshunt component.""" 5 | return True 6 | -------------------------------------------------------------------------------- /assets/377777963-13886d0d-30d3-4d62-821f-5db632fde90d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxlin1/PekawayVANPICORE_homeassistant/HEAD/assets/377777963-13886d0d-30d3-4d62-821f-5db632fde90d.png -------------------------------------------------------------------------------- /assets/377779266-6ce55e0e-9ff7-4df2-9c88-53bf6e7bb89c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxlin1/PekawayVANPICORE_homeassistant/HEAD/assets/377779266-6ce55e0e-9ff7-4df2-9c88-53bf6e7bb89c.png -------------------------------------------------------------------------------- /assets/377796060-22ff2178-28d7-4c6b-86b3-a63f1ed0face.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxlin1/PekawayVANPICORE_homeassistant/HEAD/assets/377796060-22ff2178-28d7-4c6b-86b3-a63f1ed0face.png -------------------------------------------------------------------------------- /custom_components/ads_waterlevel/__init__.py: -------------------------------------------------------------------------------- 1 | from homeassistant.core import HomeAssistant 2 | from homeassistant.helpers.discovery import load_platform 3 | 4 | DOMAIN = "ads_waterlevel" 5 | 6 | def setup(hass: HomeAssistant, config: dict): 7 | # Plattform-basierte Einrichtung 8 | return True 9 | -------------------------------------------------------------------------------- /.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@v3" 14 | - uses: home-assistant/actions/hassfest@master -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PekawayVANPICORE Component", 3 | "content_in_root": false, 4 | "render_readme": true, 5 | "image": "images/assets.png", 6 | "domains": [ 7 | "ads_waterlevel", 8 | "mcp23017", 9 | "mpu650", 10 | "onewire_sysbus", 11 | "smartshunt", 12 | "victron_component" 13 | ], 14 | "country": "DE" 15 | } 16 | -------------------------------------------------------------------------------- /custom_components/mpu650/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | _LOGGER = logging.getLogger(__name__) 4 | _LOGGER.info("MPU6050 integration is initializing") 5 | 6 | async def async_setup(hass, config): 7 | _LOGGER.debug("MPU6050 integration setup complete") 8 | return True 9 | 10 | async def async_setup_entry(hass, entry): 11 | _LOGGER.info("MPU6050 integration setup from entry complete") 12 | return True 13 | -------------------------------------------------------------------------------- /custom_components/mpu650/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "mpu650", 3 | "name": "MPU6050 Sensor", 4 | "codeowners": ["@maxlin1"], 5 | "config_flow": false, 6 | "dependencies": [], 7 | "documentation": "https://github.com/maxlin1/homeassistant_PekawayVANPICORE", 8 | "integration_type": "hub", 9 | "iot_class": "local_polling", 10 | "requirements": ["smbus2", "mpu6050-raspberrypi==1.1"], 11 | "version": "1.0" 12 | } -------------------------------------------------------------------------------- /custom_components/victron_mppt/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "victron_mppt", 3 | "name": "Victron MPPT 75/15", 4 | "codeowners": ["@maxlin1"], 5 | "dependencies": [], 6 | "documentation": "https://github.com/maxlin1/homeassistant_PekawayVANPICORE", 7 | "iot_class": "local_push", 8 | "issue_tracker": "https://github.com/maxlin1/homeassistant_PekawayVANPICORE/issues", 9 | "requirements": ["pyserial"], 10 | "version": "1.0.0" 11 | } -------------------------------------------------------------------------------- /custom_components/mcp23017/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "mcp23017", 3 | "name": "MCP23017 Digital I/O Expander", 4 | "codeowners": ["@jpcornil-git"], 5 | "config_flow": true, 6 | "dependencies": [], 7 | "documentation": "https://github.com/jpcornil-git/HA-mcp23017", 8 | "iot_class": "local_push", 9 | "issue_tracker": "https://github.com/jpcornil-git/HA-mcp23017/issues", 10 | "requirements": ["smbus2>=0.3.0"], 11 | "version": "1.0.0" 12 | } -------------------------------------------------------------------------------- /custom_components/onewire_sysbus/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "onewire_sysbus", 3 | "name": "1-Wire SysBus", 4 | "codeowners": ["@thecode"], 5 | "config_flow": true, 6 | "documentation": "https://github.com/thecode/ha-onewire-sysbus", 7 | "iot_class": "local_polling", 8 | "issue_tracker": "https://github.com/thecode/ha-onewire-sysbus/issues", 9 | "loggers": ["pi1wire"], 10 | "requirements": ["pi1wire==0.2.0"], 11 | "version": "1.0.0" 12 | } -------------------------------------------------------------------------------- /custom_components/onewire_sysbus/model.py: -------------------------------------------------------------------------------- 1 | """Type definitions for 1-Wire integration.""" 2 | from __future__ import annotations 3 | 4 | from dataclasses import dataclass 5 | 6 | from pi1wire import OneWireInterface 7 | 8 | from homeassistant.helpers.entity import DeviceInfo 9 | 10 | 11 | @dataclass 12 | class OWDeviceDescription: 13 | """1-Wire SysBus device description class.""" 14 | 15 | device_info: DeviceInfo 16 | interface: OneWireInterface 17 | -------------------------------------------------------------------------------- /custom_components/victron_smartshunt/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "victron_smartshunt", 3 | "name": "Victron SmartShunt 500A 50mV", 4 | "codeowners": ["@maxlin1"], 5 | "dependencies": [], 6 | "documentation": "https://github.com/maxlin1/homeassistant_PekawayVANPICORE", 7 | "iot_class": "local_push", 8 | "issue_tracker": "https://github.com/maxlin1/homeassistant_PekawayVANPICORE/issues", 9 | "requirements": ["pyserial"], 10 | "version": "1.0.0" 11 | } -------------------------------------------------------------------------------- /custom_components/onewire_sysbus/strings.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" 5 | }, 6 | "error": { 7 | "invalid_path": "Directory not found." 8 | }, 9 | "step": { 10 | "user": { 11 | "data": { 12 | "mount_dir": "Mount directory" 13 | }, 14 | "title": "Set up 1-Wire" 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /custom_components/ads_waterlevel/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "ads_waterlevel", 3 | "name": "ADS Water Level", 4 | "version": "1.2.0", 5 | "documentation": "https://github.com/maxlin1/homeassistant_PekawayVANPICORE", 6 | "issue_tracker": "https://github.com/maxlin1/homeassistant_PekawayVANPICORE/issues", 7 | "codeowners": ["@maxlin1"], 8 | "iot_class": "local_polling", 9 | "dependencies": [], 10 | "requirements": ["smbus2>=0.4.2", "aiofiles>=23.1.0"] 11 | } 12 | -------------------------------------------------------------------------------- /custom_components/onewire_sysbus/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "Device is already configured" 5 | }, 6 | "error": { 7 | "invalid_path": "Directory not found." 8 | }, 9 | "step": { 10 | "user": { 11 | "data": { 12 | "mount_dir": "Mount directory" 13 | }, 14 | "title": "Set up 1-Wire" 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /custom_components/onewire_sysbus/translations/sk.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "Zariadenie je už nakonfigurované" 5 | }, 6 | "error": { 7 | "invalid_path": "Adresár nenájdený." 8 | }, 9 | "step": { 10 | "user": { 11 | "data": { 12 | "mount_dir": "Pripojiť adresár" 13 | }, 14 | "title": "Nastaviť 1-Wire" 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /custom_components/onewire_sysbus/translations/pt-BR.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "O dispositivo já está configurado" 5 | }, 6 | "error": { 7 | "invalid_path": "Diretório não encontrado." 8 | }, 9 | "step": { 10 | "user": { 11 | "data": { 12 | "mount_dir": "Diretório de montagem" 13 | }, 14 | "title": "Configurar 1-Wire" 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /custom_components/onewire_sysbus/const.py: -------------------------------------------------------------------------------- 1 | """Constants for 1-Wire component.""" 2 | from __future__ import annotations 3 | 4 | from homeassistant.const import Platform 5 | 6 | CONF_MOUNT_DIR = "mount_dir" 7 | DEFAULT_SYSBUS_MOUNT_DIR = "/sys/bus/w1/devices/" 8 | 9 | DOMAIN = "onewire_sysbus" 10 | 11 | DEVICE_SUPPORT_SYSBUS = ["10", "22", "28", "3B", "42"] 12 | 13 | MANUFACTURER_MAXIM = "Maxim Integrated" 14 | MANUFACTURER_HOBBYBOARDS = "Hobby Boards" 15 | MANUFACTURER_EDS = "Embedded Data Systems" 16 | 17 | READ_MODE_FLOAT = "float" 18 | 19 | PLATFORMS = [Platform.SENSOR] 20 | -------------------------------------------------------------------------------- /custom_components/mcp23017/const.py: -------------------------------------------------------------------------------- 1 | """Constants for MCP23017 integration.""" 2 | DOMAIN = "mcp23017" 3 | 4 | MODE_UP = "UP" 5 | MODE_DOWN = "NONE" 6 | 7 | CONF_I2C_ADDRESS = "i2c_address" 8 | CONF_PINS = "pins" 9 | 10 | CONF_INVERT_LOGIC = "invert_logic" 11 | CONF_PULL_MODE = "pull_mode" 12 | CONF_HW_SYNC = "hw_sync" 13 | 14 | CONF_FLOW_PLATFORM = "platform" 15 | CONF_FLOW_PIN_NUMBER = "pin_number" 16 | CONF_FLOW_PIN_NAME = "pin_name" 17 | 18 | DEFAULT_SCAN_RATE = 0.1 # seconds 19 | DEFAULT_I2C_BUS = 1 # use /dev/i2c-{DEFAULT_I2C_BUS} 20 | DEFAULT_I2C_ADDRESS = 0x20 21 | 22 | DEFAULT_INVERT_LOGIC = False 23 | DEFAULT_PULL_MODE = MODE_UP 24 | DEFAULT_HW_SYNC = True 25 | -------------------------------------------------------------------------------- /custom_components/mpu650/button.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from homeassistant.components.button import ButtonEntity 3 | 4 | _LOGGER = logging.getLogger(__name__) 5 | 6 | class CustomButton(ButtonEntity): 7 | def __init__(self, name, manager): 8 | self._name = name 9 | self._manager = manager 10 | 11 | @property 12 | def name(self): 13 | return self._name 14 | 15 | def press(self): 16 | self._manager.calibrate() 17 | 18 | @property 19 | def unique_id(self): 20 | return "input_button.richte_auf_0_aus" 21 | 22 | @property 23 | def icon(self): 24 | return "mdi:play" 25 | 26 | def setup_platform(hass, config, add_entities, discovery_info=None): 27 | manager = hass.data.get("mpu6050_sensor_manager") 28 | custom_button = CustomButton("Richte Auf 0 Aus", manager) 29 | add_entities([custom_button]) 30 | -------------------------------------------------------------------------------- /custom_components/onewire_sysbus/diagnostics.py: -------------------------------------------------------------------------------- 1 | """Diagnostics support for 1-Wire.""" 2 | from __future__ import annotations 3 | 4 | from typing import Any 5 | 6 | from homeassistant.config_entries import ConfigEntry 7 | from homeassistant.core import HomeAssistant 8 | 9 | from .const import DOMAIN 10 | from .onewirehub import OneWireHub 11 | 12 | 13 | async def async_get_config_entry_diagnostics( 14 | hass: HomeAssistant, entry: ConfigEntry 15 | ) -> dict[str, Any]: 16 | """Return diagnostics for a config entry.""" 17 | onewirehub: OneWireHub = hass.data[DOMAIN][entry.entry_id] 18 | 19 | return { 20 | "entry": { 21 | "title": entry.title, 22 | "data": dict(entry.data), 23 | }, 24 | "devices": [device_details.device_info for device_details in onewirehub.devices] 25 | if onewirehub.devices 26 | else [], 27 | } 28 | -------------------------------------------------------------------------------- /custom_components/mcp23017/strings.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "title": "Define New Entity", 6 | "data": { 7 | "i2c_address": "I2C address", 8 | "platform": "Platform", 9 | "pin_number": "Pin number", 10 | "pin_name": "Pin name" 11 | } 12 | } 13 | }, 14 | "abort": { 15 | "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", 16 | "cannot_create": "Unable to create MCP23017 device with specified parameters" 17 | } 18 | }, 19 | "options": { 20 | "step": { 21 | "init": { 22 | "title": "Define Entity Properties", 23 | "data": { 24 | "invert_logic": "Invert logic", 25 | "pull_mode": "Pull mode", 26 | "hw_sync": "Initial value from hardware" 27 | } 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /custom_components/mcp23017/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "Device is already configured", 5 | "cannot_create": "Unable to create MCP23017 device with specified parameters" 6 | }, 7 | "step": { 8 | "user": { 9 | "data": { 10 | "i2c_address": "I2C address", 11 | "pin_name": "Pin name", 12 | "pin_number": "Pin number", 13 | "platform": "Platform" 14 | }, 15 | "title": "Define New Entity" 16 | } 17 | } 18 | }, 19 | "options": { 20 | "step": { 21 | "init": { 22 | "data": { 23 | "invert_logic": "Invert logic", 24 | "pull_mode": "Pull mode", 25 | "hw_sync": "Initial value from hardware" 26 | }, 27 | "title": "Define Entity Properties" 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /custom_components/onewire_sysbus/__init__.py: -------------------------------------------------------------------------------- 1 | """The 1-Wire SysBus component.""" 2 | 3 | from homeassistant.config_entries import ConfigEntry 4 | from homeassistant.core import HomeAssistant 5 | 6 | from .const import DOMAIN, PLATFORMS 7 | from .onewirehub import OneWireHub 8 | 9 | 10 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 11 | """Set up a 1-Wire hub for a config entry.""" 12 | hass.data.setdefault(DOMAIN, {}) 13 | 14 | onewirehub = OneWireHub(hass) 15 | await onewirehub.initialize(entry) 16 | 17 | hass.data[DOMAIN][entry.entry_id] = onewirehub 18 | 19 | await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) 20 | 21 | return True 22 | 23 | 24 | async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: 25 | """Unload a config entry.""" 26 | if unload_ok := await hass.config_entries.async_unload_platforms( 27 | config_entry, PLATFORMS 28 | ): 29 | hass.data[DOMAIN].pop(config_entry.entry_id) 30 | 31 | return unload_ok 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 maxlin1 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 | -------------------------------------------------------------------------------- /installHA.md: -------------------------------------------------------------------------------- 1 | ### Install Home Assistant ### 2 | 3 | SD Karte Flashen: 4 | 5 | https://www.home-assistant.io/installation/raspberrypi#install-home-assistant-operating-system 6 | 7 | - SD Karte in den RPI4 am PeakawayBoard einstetzen 8 | - Strom anschließen 9 | - Wichtig der RPI4 muss mit einem Router mit Internet verbunden sein! 10 | - 5-10min je nach RPI 4/8GB warten 11 | 12 | ### Config Home Assistant ### 13 | 14 | Im Browser 'http://IP-VOM-RPI:8123/' eingeben, ist euer DNS im Router richtig konfiguriert sollte auch 'http://homeassistant.local:8123' gehen. 15 | 16 | Konfig Fenster Homeassistant 17 | 18 | Die Schritte zeige ich jetzt nicht im einzelnen sind aber selbterklärend und ihr müsst einfach Homeassistant mit Name, Passwort usw. konfigurieren. 19 | 20 | Anschließend Sehen wir eine noch relativ leere Oberfläche. 21 | 22 | Erster Start Homeassistant 23 | 24 | 25 | ### Erweiterte Modus einschalten 26 | 27 | Unten Links auf den 'Usernamen' klicken um dann den -> Erweiterten Modus einschalten. 28 | 29 | Erweiterter Modus 30 | 31 | 32 | -> Neustart! Die installation ist Geschafft! 🥳 33 | 34 | -> Nun geht es in der [info.md](./info.md) weiter 35 | -------------------------------------------------------------------------------- /custom_components/onewire_sysbus/onewire_entities.py: -------------------------------------------------------------------------------- 1 | """Support for 1-Wire entities.""" 2 | from __future__ import annotations 3 | 4 | from dataclasses import dataclass 5 | from typing import Any 6 | 7 | from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription 8 | from homeassistant.helpers.typing import StateType 9 | 10 | 11 | @dataclass 12 | class OneWireEntityDescription(EntityDescription): 13 | """Class describing OneWire entities.""" 14 | 15 | read_mode: str | None = None 16 | 17 | 18 | class OneWireEntity(Entity): 19 | """Implementation of a 1-Wire entity.""" 20 | 21 | entity_description: OneWireEntityDescription 22 | 23 | def __init__( 24 | self, 25 | description: OneWireEntityDescription, 26 | device_id: str, 27 | device_info: DeviceInfo, 28 | device_file: str, 29 | name: str, 30 | ) -> None: 31 | """Initialize the entity.""" 32 | self.entity_description = description 33 | self._attr_unique_id = f"/{device_id}/{description.key}" 34 | self._attr_device_info = device_info 35 | self._attr_name = name 36 | self._device_file = device_file 37 | self._state: StateType = None 38 | self._value_raw: float | None = None 39 | 40 | @property 41 | def extra_state_attributes(self) -> dict[str, Any] | None: 42 | """Return the state attributes of the entity.""" 43 | return { 44 | "device_file": self._device_file, 45 | "raw_value": self._value_raw, 46 | } 47 | -------------------------------------------------------------------------------- /custom_components/mpu650/switch.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import asyncio 3 | from homeassistant.components.switch import SwitchEntity 4 | 5 | _LOGGER = logging.getLogger(__name__) 6 | 7 | class CustomSwitch(SwitchEntity): 8 | def __init__(self, name, manager): 9 | self._name = name 10 | self._is_on = False 11 | self._manager = manager 12 | 13 | @property 14 | def name(self): 15 | return self._name 16 | 17 | @property 18 | def is_on(self): 19 | return self._is_on 20 | 21 | def turn_on(self, **kwargs): 22 | self._is_on = True 23 | #self._manager.start() 24 | self.schedule_update_ha_state() 25 | 26 | def turn_off(self, **kwargs): 27 | self._is_on = False 28 | #self._manager.stop() 29 | self.schedule_update_ha_state() 30 | 31 | @property 32 | def unique_id(self): 33 | return "mpu6050_switch" 34 | 35 | @property 36 | def icon(self): 37 | return "mdi:power" 38 | 39 | async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): 40 | # Versucht alle 2 Sekunden, den manager zu laden, bis zu 5 Mal (insgesamt 10 Sekunden) 41 | for attempt in range(5): 42 | manager = hass.data.get("mpu6050_sensor_manager") 43 | if manager: 44 | custom_switch = CustomSwitch("Schalte Ausrichtung Ein", manager) 45 | async_add_entities([custom_switch]) 46 | _LOGGER.info("MPU6050 Custom Switch erfolgreich hinzugefügt.") 47 | return 48 | else: 49 | _LOGGER.warning(f"Versuch {attempt+1}: MPU6050 Sensor Manager nicht gefunden, erneuter Versuch in 2 Sekunden...") 50 | await asyncio.sleep(2) 51 | 52 | _LOGGER.error("MPU6050 Sensor Manager konnte nicht gefunden werden. Switch wird nicht hinzugefügt.") 53 | -------------------------------------------------------------------------------- /custom_components/victron_mppt/config.py: -------------------------------------------------------------------------------- 1 | key_mapping = { 2 | "PID": {"name": "Produkt-ID", "unit": None, "icon": "mdi:identifier"}, 3 | "FW": {"name": "Firmware-Version", "unit": None, "icon": "mdi:chip","scale": 0.01, }, 4 | "SER#": {"name": "Seriennummer", "unit": None, "icon": "mdi:barcode"}, 5 | "V": {"name": "Batteriespannung", "unit": "V", "icon": "mdi:flash", "scale": 0.001, "round": 2}, 6 | "I": {"name": "Batteriestrom", "unit": "A", "icon": "mdi:current-dc", "scale": 0.001 }, 7 | "VPV": {"name": "PV-Spannung", "unit": "V", "icon": "mdi:solar-power", "scale": 0.001 }, 8 | "PPV": {"name": "PV-Leistung", "unit": "W", "icon": "mdi:solar-power", "scale": 0.001 }, 9 | "CS": {"name": "Ladestatus", "unit": None, "icon": "mdi:power-settings", "scale": 0.001 }, 10 | "MPPT": {"name": "MPPT-Modus", "unit": None, "icon": "mdi:solar-power" }, 11 | "OR": {"name": "Betriebsstatus", "unit": None, "icon": "mdi:information"}, 12 | "ERR": {"name": "Fehlercode", "unit": None, "icon": "mdi:alert"}, 13 | "LOAD": {"name": "Lastausgang", "unit": None, "icon": "mdi:power"}, 14 | "IL": {"name": "Laststrom", "unit": "A", "icon": "mdi:current-dc", "scale": 0.001 }, 15 | "H19": {"name": "Gesamtertrag", "unit": "kWh", "icon": "mdi:calendar", "scale": 0.01, "round": 2 }, 16 | "H20": {"name": "Ertrag heute", "unit": "kWh", "icon": "mdi:calendar-today", "scale": 0.001 }, 17 | "H21": {"name": "Ertrag gestern", "unit": "kWh", "icon": "mdi:calendar-today", "scale": 0.001 }, 18 | "H22": {"name": "Ertrag letzte 30 Tage", "unit": "kWh", "icon": "mdi:calendar-month", "scale": 0.001 }, 19 | "H23": {"name": "Tagesmaximum PV-Spannung", "unit": "V", "icon": "mdi:flash", "scale": 0.001 }, 20 | "HSDS": {"name": "Tageszählstand", "unit": None, "icon": "mdi:counter", "scale": 0.001 }, 21 | } 22 | -------------------------------------------------------------------------------- /custom_components/onewire_sysbus/config_flow.py: -------------------------------------------------------------------------------- 1 | """Config flow for 1-Wire SysBus component.""" 2 | from __future__ import annotations 3 | 4 | from typing import Any 5 | 6 | import voluptuous as vol 7 | 8 | from homeassistant.config_entries import ConfigFlow 9 | from homeassistant.core import HomeAssistant 10 | from homeassistant.data_entry_flow import FlowResult 11 | 12 | from .const import CONF_MOUNT_DIR, DEFAULT_SYSBUS_MOUNT_DIR, DOMAIN 13 | from .onewirehub import InvalidPath, OneWireHub 14 | 15 | DATA_SCHEMA = vol.Schema( 16 | { 17 | vol.Required(CONF_MOUNT_DIR, default=DEFAULT_SYSBUS_MOUNT_DIR): str, 18 | } 19 | ) 20 | 21 | 22 | async def validate_input_mount_dir( 23 | hass: HomeAssistant, data: dict[str, Any] 24 | ) -> dict[str, str]: 25 | """Validate the user input allows us to connect.""" 26 | hub = OneWireHub(hass) 27 | mount_dir = data[CONF_MOUNT_DIR] 28 | 29 | # Raises InvalidDir exception on failure 30 | await hub.check_mount_dir(mount_dir) 31 | 32 | return {"title": mount_dir} 33 | 34 | 35 | class OneWireFlowHandler(ConfigFlow, domain=DOMAIN): # type: ignore 36 | """Handle 1-Wire config flow.""" 37 | 38 | VERSION = 1 39 | 40 | def __init__(self) -> None: 41 | """Initialize 1-Wire config flow.""" 42 | 43 | async def async_step_user( 44 | self, user_input: dict[str, Any] | None = None 45 | ) -> FlowResult: 46 | """Handle 1-Wire config flow start.""" 47 | errors: dict[str, str] = {} 48 | if user_input is not None: 49 | # Prevent duplicate entries 50 | await self.async_set_unique_id(user_input[CONF_MOUNT_DIR]) 51 | self._abort_if_unique_id_configured() 52 | 53 | try: 54 | info = await validate_input_mount_dir(self.hass, user_input) 55 | except InvalidPath: 56 | errors["base"] = "invalid_path" 57 | else: 58 | return self.async_create_entry(title=info["title"], data=user_input) 59 | 60 | return self.async_show_form( 61 | step_id="user", 62 | data_schema=DATA_SCHEMA, 63 | errors=errors, 64 | ) 65 | -------------------------------------------------------------------------------- /custom_components/mpu650/mpuAngle.py: -------------------------------------------------------------------------------- 1 | import smbus2 2 | import math 3 | import time 4 | import json 5 | import argparse 6 | 7 | # MPU6050 Registers 8 | MPU6050_ADDR = 0x69 9 | MPU6050_PWR_MGMT_1 = 0x6B 10 | MPU6050_TEMP_OUT_H = 0x41 11 | MPU6050_ACCEL_XOUT_H = 0x3B 12 | MPU6050_ACCEL_YOUT_H = 0x3D 13 | MPU6050_ACCEL_ZOUT_H = 0x3F 14 | MPU6050_GYRO_XOUT_H = 0x43 15 | MPU6050_GYRO_YOUT_H = 0x45 16 | MPU6050_GYRO_ZOUT_H = 0x47 17 | 18 | # Configuration 19 | bus = smbus2.SMBus(1) # or 0 for RPi 1 20 | bus.write_byte_data(MPU6050_ADDR, MPU6050_PWR_MGMT_1, 0) 21 | 22 | def read_raw_data(addr): 23 | # Read raw data in a single transaction 24 | high = bus.read_i2c_block_data(MPU6050_ADDR, addr, 2) 25 | value = (high[0] << 8) | high[1] 26 | if value > 32768: 27 | value -= 65536 28 | return value 29 | 30 | def parse_args(): 31 | parser = argparse.ArgumentParser(description="MPU6050 Data Logger") 32 | parser.add_argument("--time", type=float, default=1, help="Sleep time in seconds (default: 1)") 33 | return parser.parse_args() 34 | 35 | args = parse_args() 36 | 37 | while True: 38 | start_time = time.time() 39 | 40 | # Read sensor data 41 | accel_x, accel_y, accel_z = [read_raw_data(addr) for addr in (MPU6050_ACCEL_XOUT_H, MPU6050_ACCEL_YOUT_H, MPU6050_ACCEL_ZOUT_H)] 42 | gyro_x, gyro_y, gyro_z = [read_raw_data(addr) for addr in (MPU6050_GYRO_XOUT_H, MPU6050_GYRO_YOUT_H, MPU6050_GYRO_ZOUT_H)] 43 | temp = read_raw_data(MPU6050_TEMP_OUT_H) 44 | 45 | # Calculate angles 46 | x_angle = math.atan(accel_x / 16384.0) * (180 / math.pi) 47 | y_angle = math.atan(accel_y / 16384.0) * (180 / math.pi) 48 | 49 | # Prepare data dictionary 50 | data = { 51 | "x_angle": x_angle, 52 | "y_angle": y_angle, 53 | "accel_x_raw": accel_x, 54 | "accel_y_raw": accel_y, 55 | "accel_z_raw": accel_z, 56 | "gyro_x_raw": gyro_x, 57 | "gyro_y_raw": gyro_y, 58 | "gyro_z_raw": gyro_z, 59 | "mpu_temp": (temp / 340.0) + 36.53 # Temperature formula for MPU6050 60 | } 61 | 62 | # Print JSON data 63 | print(json.dumps(data)) 64 | 65 | # Adjust sleep time to optimize logging frequency 66 | sleep_time = max(0, args.time - (time.time() - start_time)) 67 | time.sleep(sleep_time) 68 | -------------------------------------------------------------------------------- /custom_components/victron_smartshunt/config.py: -------------------------------------------------------------------------------- 1 | key_mapping = { 2 | "V": {"name": "VS_Batteriespannung", "unit": "V", "icon": "mdi:flash", "scale": 0.001, "round":2}, 3 | "I": {"name": "VS_Batteriestrom", "unit": "A", "icon": "mdi:current-dc", "scale": 0.001, "round":2 }, 4 | "P": {"name": "VS_Leistung", "unit": "W", "icon": "mdi:flash" }, 5 | "CE": {"name": "VS_Verbrauchte Ah", "unit": "Ah", "icon": "mdi:battery-minus", "scale": 0.001, "round":1 }, 6 | "FW": {"name": "VS_Firmware-Version", "unit": None, "icon": "mdi:chip"}, 7 | "SER": {"name": "VS_Seriennummer", "unit": None, "icon": "mdi:barcode"}, 8 | "SOC": {"name": "VS_Ladezustand", "unit": "%", "icon": "mdi:battery", "scale": 0.1 }, 9 | "TTG": {"name": "VS_Verbleibende Zeit", "unit": "h", "icon": "mdi:timer-outline", "scale": 0.016666666666666666, "round":2}, 10 | "Alarm": {"name": "VS_Alarmstatus", "unit": None, "icon": "mdi:alert" }, 11 | "H1": {"name": "VS_Tiefste Entladung", "unit": "Ah", "icon": "mdi:history", "scale": 0.001,"round":1 }, 12 | "H2": {"name": "VS_Letzte Entladung", "unit": "Ah", "icon": "mdi:history", "scale": 0.001,"round":1}, 13 | "H3": {"name": "VS_Durchschnittliche Entladung", "unit": "Ah", "icon": "mdi:battery-arrow-down", "scale": 0.001,"round":1}, 14 | "H7": {"name": "VS_Min. Batteriespannung", "unit": "V", "icon": "mdi:flash-outline", "scale": 0.001, "round":2 }, 15 | "H8": {"name": "VS_Zeit seit letztem vollständigem Aufladen", "unit": "h", "icon": "mdi:timer-sand", "scale": 0.016666666666666666, "round":2}, 16 | "H15": {"name": "VS_Min. Spannung Starter", "unit": "V", "icon": "mdi:battery-arrow-down-outline", "scale": 0.001, "round":2}, 17 | "H16": {"name": "VS_Max. Spannung Starter", "unit": "V", "icon": "mdi:battery-arrow-up-outline", "scale": 0.001, "round":2}, 18 | "H17": {"name": "VS_Entladene Energie", "unit": "kWh", "icon": "mdi:battery-arrow-down", "scale": 0.01, "round":1}, 19 | "H18": {"name": "VS_Geladene Energie", "unit": "kWh", "icon": "mdi:flash", "scale": 0.01, "round":1}, 20 | "AR": {"name": "VS_Alarmursache", "unit": None, "icon": "mdi:alert-circle"}, 21 | "PID": {"name": "VS_Produkt-ID", "unit": None, "icon": "mdi:identifier"}, 22 | "BMV": {"name": "VS_Batterie Monitor", "unit": None, "icon": "mdi:battery"}, 23 | "H4": {"name": "VS_Ladezyklen insgesamt", "unit": None, "icon": "mdi:refresh"}, 24 | "H5": {"name": "VS_Ladezyklen", "unit": None, "icon": "mdi:battery-charging"}, 25 | "H6": {"name": "VS_Max. Batteriespannung", "unit": "V", "icon": "mdi:flash-outline", "scale": 0.001, "round":2}, 26 | "H9": {"name": "VS_Automatische Synchronisationen", "unit": None, "icon": "mdi:sync"}, 27 | "H10": {"name": "VS_Unterspannung-Alarme", "unit": None, "icon": "mdi:alert-octagram"}, 28 | "H11": {"name": "VS_Überspannung-Alarme", "unit": None, "icon": "mdi:alert-octagram"}, 29 | "H12": {"name": "VS_Unterspannung-Starter-Alarme", "unit": None, "icon": "mdi:alert-circle-outline"}, 30 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # homeassistant_PekawayVANPICORE 2 | VAN PI CORE Board Integration for Home Assistant 3 | This custom component integrates the VAN PI CORE Board with Home Assistant, allowing you to monitor and control your van's systems through the Home Assistant interface. 4 | 5 | Diese benutzerdefinierte Komponente integriert das VAN PI CORE Board mit Home Assistant und ermöglicht es dir, die Systeme deines Vans über die Home Assistant zu überwachen und zu steuern. 6 | 7 | ## Disclaimer 8 | 9 | Ich habe mein Bestes getan, um diese Komponente so zuverlässig und nützlich wie möglich zu gestalten. Bitte beachte jedoch: 10 | 11 | - Die Nutzung dieser Komponente erfolgt auf eigenes Risiko. 12 | - Ich kann leider keine Haftung für etwaige Schäden übernehmen, die durch die Verwendung oder Nichtverwendung entstehen könnten. 13 | - Dies gilt für alle Arten von Schäden, sei es direkt oder indirekt. 14 | 15 | Bei mir funktioniert die Komponente einwandfrei, und ich bin zuversichtlich, dass sie auch bei dir laufen wird! 😊 16 | Solltest du dennoch auf Probleme stoßen oder Fragen haben, zögere bitte nicht, mich zu kontaktieren. Ich helfe im Rahmen meiner Möglichkeiten gerne weiter und freue mich über dein Feedback! 17 | Eine Antwort kann jedoch auch mal mehrere Tage dauern. 18 | 19 | ## Features 20 | 21 | - Integriert Inputs 1-8 22 | - Integriert Relais 1-8 23 | - Integriert MPU6050 (Beschleunigungs- und Lagersensor für die Van Ausrichtung) 24 | - Integriert ADS1115 (Wasserlevel) 25 | - Integriert bis zu 5 1-Wire Sensoren (als Temperatursensoren) 26 | - Integriert UART 1 (RJ45) 27 | - Integriert UART 2 (RJ11 LIN) 28 | - Integriert UART 4 (MPPT 75/15 Victron) 29 | - Integriert UART 5 (SmartShunt 500A/50mV Victron) 30 | 31 | INFO: UART3 gibt es nicht auf dem Pin lauscht der 1-Wire Sensor! 32 | 33 | ## 1. Installation von Homeassistant 34 | 35 | Wer noch nie Homeassistant selbst installiert hat findet in der [installHA.md](./installHA.md) die Anleitung Stand November 2025. 36 | 37 | ## 2. Configuration und Installation 38 | 39 | Die Configuration der HACS Komponente in HomeAssistant findet ihr in der [info.md](./info.md) 40 | 41 | 42 | ## Contributing Mitwirkung 43 | 44 | Contributions are welcome! Please feel free to submit a Pull Request. 45 | Mitwirken ist gerne gesehen! 46 | 47 | ## License 48 | 49 | This project is licensed under the MIT License - see the LICENSE file for details. 50 | 51 | ## Acknowledgments Danksagungen 52 | 53 | - Vielen Dank an die Home Assistant-Community für ihre hervorragende Dokumentation und Beispiele. 54 | 55 | - Besonderer Dank gilt den Entwicklern des VAN PI CORE Board für ihre Hardware- und API-Dokumentation. 56 | 57 | - Vielen dan an [@MikeTsenatek](https://github.com/MikeTsenatek) ohne ihn wäre ich nicht weiter gekommen. 58 | 59 | - Danke an [@MSchroederRobert](https://github.com/schroeder-robert) für den Austausch und weitere Ideen 60 | 61 | - Danke an [@DerKleinePunk](https://github.com/DerKleinePunk) für seine Ideen und den Austausch 62 | 63 | ## Schluss Worte 64 | Viel Spass mit eurem Peakaway board in Homeassistant wenn ihr spannende Projekte umgesetzt habe teilt Sie gerne mit der Comunity im [Pekaway Forum ](https://forum.pekaway.de) oder stellt einen Pullrequest so kann die Integration immer weiter wachsen. 65 | -------------------------------------------------------------------------------- /.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 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 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 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ 163 | 164 | #Mac 165 | 166 | .DS_Store -------------------------------------------------------------------------------- /custom_components/onewire_sysbus/onewirehub.py: -------------------------------------------------------------------------------- 1 | """Hub for communication with 1-Wire mount_dir via SysBus.""" 2 | from __future__ import annotations 3 | 4 | import logging 5 | import os 6 | from typing import TYPE_CHECKING 7 | 8 | from pi1wire import Pi1Wire 9 | 10 | from homeassistant.config_entries import ConfigEntry 11 | from homeassistant.const import ( 12 | ATTR_IDENTIFIERS, 13 | ATTR_MANUFACTURER, 14 | ATTR_MODEL, 15 | ATTR_NAME, 16 | ATTR_VIA_DEVICE, 17 | ) 18 | from homeassistant.core import HomeAssistant 19 | from homeassistant.exceptions import HomeAssistantError 20 | from homeassistant.helpers import device_registry as dr 21 | from homeassistant.helpers.entity import DeviceInfo 22 | 23 | from .const import ( 24 | CONF_MOUNT_DIR, 25 | DEVICE_SUPPORT_SYSBUS, 26 | DOMAIN, 27 | MANUFACTURER_EDS, 28 | MANUFACTURER_HOBBYBOARDS, 29 | MANUFACTURER_MAXIM, 30 | ) 31 | from .model import OWDeviceDescription 32 | 33 | DEVICE_MANUFACTURER = { 34 | "7E": MANUFACTURER_EDS, 35 | "EF": MANUFACTURER_HOBBYBOARDS, 36 | } 37 | 38 | _LOGGER = logging.getLogger(__name__) 39 | 40 | 41 | class OneWireHub: 42 | """Hub to communicate with SysBus or OWServer.""" 43 | 44 | def __init__(self, hass: HomeAssistant) -> None: 45 | """Initialize.""" 46 | self.hass = hass 47 | self.pi1proxy: Pi1Wire | None = None 48 | self.devices: list[OWDeviceDescription] | None = None 49 | 50 | async def check_mount_dir(self, mount_dir: str) -> None: 51 | """Test that the mount_dir is a valid path.""" 52 | if not await self.hass.async_add_executor_job(os.path.isdir, mount_dir): 53 | raise InvalidPath 54 | self.pi1proxy = Pi1Wire(mount_dir) 55 | 56 | async def initialize(self, config_entry: ConfigEntry) -> None: 57 | """Initialize a config entry.""" 58 | mount_dir = config_entry.data[CONF_MOUNT_DIR] 59 | await self.check_mount_dir(mount_dir) 60 | await self.discover_devices() 61 | 62 | if TYPE_CHECKING: 63 | assert self.devices 64 | # Register discovered devices on Hub 65 | device_registry = dr.async_get(self.hass) 66 | for device in self.devices: 67 | device_info: DeviceInfo = device.device_info 68 | device_registry.async_get_or_create( 69 | config_entry_id=config_entry.entry_id, 70 | identifiers=device_info[ATTR_IDENTIFIERS], 71 | manufacturer=device_info[ATTR_MANUFACTURER], 72 | model=device_info[ATTR_MODEL], 73 | name=device_info[ATTR_NAME], 74 | via_device=device_info.get(ATTR_VIA_DEVICE), 75 | ) 76 | 77 | async def discover_devices(self) -> None: 78 | """Discover all devices.""" 79 | if self.devices is None: 80 | self.devices = await self.hass.async_add_executor_job( 81 | self._discover_devices_sysbus 82 | ) 83 | 84 | def _discover_devices_sysbus(self) -> list[OWDeviceDescription]: 85 | """Discover all sysbus devices.""" 86 | devices: list[OWDeviceDescription] = [] 87 | assert self.pi1proxy 88 | all_sensors = self.pi1proxy.find_all_sensors() 89 | if not all_sensors: 90 | _LOGGER.error( 91 | "No onewire sensor found. Check if dtoverlay=w1-gpio " 92 | "is in your /boot/config.txt. " 93 | "Check the mount_dir parameter if it's defined" 94 | ) 95 | for interface in all_sensors: 96 | device_family = interface.mac_address[:2] 97 | device_id = f"{device_family}-{interface.mac_address[2:]}" 98 | if device_family not in DEVICE_SUPPORT_SYSBUS: 99 | _LOGGER.warning( 100 | "Ignoring unknown device family (%s) found for device %s", 101 | device_family, 102 | device_id, 103 | ) 104 | continue 105 | device_info: DeviceInfo = { 106 | ATTR_IDENTIFIERS: {(DOMAIN, device_id)}, 107 | ATTR_MANUFACTURER: DEVICE_MANUFACTURER.get( 108 | device_family, MANUFACTURER_MAXIM 109 | ), 110 | ATTR_MODEL: device_family, 111 | ATTR_NAME: device_id, 112 | } 113 | device = OWDeviceDescription( 114 | device_info=device_info, 115 | interface=interface, 116 | ) 117 | devices.append(device) 118 | return devices 119 | 120 | 121 | class InvalidPath(HomeAssistantError): 122 | """Error to indicate the path is invalid.""" 123 | -------------------------------------------------------------------------------- /custom_components/victron_mppt/test.py: -------------------------------------------------------------------------------- 1 | import serial 2 | import time 3 | 4 | # Konfiguration aus config.h 5 | PORT = '/dev/ttyAMA4' # Serielle Port-Konfiguration 6 | BAUDRATE = 19200 7 | TIMEOUT = 1 8 | 9 | # Konfigurationswerte aus config.h 10 | BUFFSIZE = 32 # Puffergröße 11 | VALUE_BYTES = 33 # Größe der Werte in Bytes 12 | LABEL_BYTES = 9 # Größe der Labels in Bytes 13 | NUM_KEYWORDS = 18 # Anzahl der Schlüsselwörter für MPPT 75 | 10 14 | 15 | # Definition der Schlüsselwörter für MPPT 75 | 10 16 | keywords = [ 17 | "PID", "FW", "SER#", "V", "I", "VPV", "PPV", "CS", "ERR", "LOAD", 18 | "IL", "H19", "H20", "H21", "H22", "H23", "HSDS", "Checksum" 19 | ] 20 | 21 | # Umschlüsselungstabelle 22 | key_mapping = { 23 | "PID": {"name": "Produkt-ID", "unit": None, "icon": "mdi:identifier", "update_interval": 3600}, 24 | "FW": {"name": "Firmware-Version", "unit": None, "icon": "mdi:chip", "update_interval": 3600}, 25 | "SER#": {"name": "Seriennummer", "unit": None, "icon": "mdi:barcode", "update_interval": 3600}, 26 | "V": {"name": "Batteriespannung", "unit": "V", "icon": "mdi:flash", "update_interval": 30}, 27 | "I": {"name": "Batteriestrom", "unit": "A", "icon": "mdi:current-dc", "update_interval": 30}, 28 | "VPV": {"name": "PV-Spannung", "unit": "V", "icon": "mdi:solar-power", "update_interval": 30}, 29 | "PPV": {"name": "PV-Leistung", "unit": "W", "icon": "mdi:solar-power", "update_interval": 30}, 30 | "CS": {"name": "Ladestatus", "unit": None, "icon": "mdi:power-settings", "update_interval": 30}, 31 | "MPPT": {"name": "MPPT-Modus", "unit": None, "icon": "mdi:solar-power", "update_interval": 30}, 32 | "OR": {"name": "Betriebsstatus", "unit": None, "icon": "mdi:information", "update_interval": 3600}, 33 | "ERR": {"name": "Fehlercode", "unit": None, "icon": "mdi:alert", "update_interval": 30}, 34 | "LOAD": {"name": "Lastausgang", "unit": None, "icon": "mdi:power", "update_interval": 30}, 35 | "IL": {"name": "Laststrom", "unit": "A", "icon": "mdi:current-dc", "update_interval": 30}, 36 | "H19": {"name": "Gesamtertrag", "unit": "kWh", "icon": "mdi:calendar", "update_interval": 3600}, 37 | "H20": {"name": "Ertrag heute", "unit": "kWh", "icon": "mdi:calendar-today", "update_interval": 3600}, 38 | "H21": {"name": "Ertrag gestern", "unit": "kWh", "icon": "mdi:calendar-yesterday", "update_interval": 3600}, 39 | "H22": {"name": "Ertrag letzte 30 Tage", "unit": "kWh", "icon": "mdi:calendar-month", "update_interval": 3600}, 40 | "H23": {"name": "Tagesmaximum PV-Spannung", "unit": "V", "icon": "mdi:flash", "update_interval": 3600}, 41 | "HSDS": {"name": "Tageszählstand", "unit": None, "icon": "mdi:counter", "update_interval": 3600} 42 | } 43 | 44 | # Verbindung zur seriellen Schnittstelle herstellen 45 | try: 46 | ser = serial.Serial(PORT, BAUDRATE, timeout=TIMEOUT) 47 | print(f"Verbindung zu {PORT} hergestellt.") 48 | except serial.SerialException as e: 49 | print(f"Fehler beim Öffnen der seriellen Schnittstelle: {e}") 50 | exit(1) 51 | 52 | # Letzter bekannter Zustand der Daten 53 | last_data = {} 54 | 55 | def parse_line(line): 56 | """ 57 | Parst eine Zeile des VE.Direct-Protokolls und gibt ein Tupel aus (Schlüssel, Wert) zurück. 58 | """ 59 | try: 60 | key, value = line.split('\t') 61 | return key, value 62 | except ValueError: 63 | return None, None 64 | 65 | def read_victron_data(): 66 | """ 67 | Liest Daten vom Victron-Gerät und gibt sie als umgeschlüsseltes Dictionary zurück. 68 | """ 69 | data = {} 70 | while True: 71 | line = ser.readline().decode('ascii').strip() 72 | if not line: 73 | continue 74 | key, value = parse_line(line) 75 | if key and value and key in key_mapping: 76 | # Umschlüsseln der Daten 77 | mapped_key = key_mapping[key]["name"] 78 | data[mapped_key] = { 79 | "value": value, 80 | "unit": key_mapping[key]["unit"], 81 | "icon": key_mapping[key]["icon"], 82 | "update_interval": key_mapping[key]["update_interval"] 83 | } 84 | if key == "Checksum": 85 | break 86 | 87 | return data 88 | 89 | def has_data_changed(new_data, old_data): 90 | """ 91 | Überprüft, ob sich die neuen Daten von den alten unterscheiden. 92 | """ 93 | for key, details in new_data.items(): 94 | if key not in old_data or old_data[key]["value"] != details["value"]: 95 | return True 96 | return False 97 | 98 | try: 99 | while True: 100 | victron_data = read_victron_data() 101 | if victron_data and has_data_changed(victron_data, last_data): 102 | print("Empfangene und geänderte Daten:") 103 | for key, details in victron_data.items(): 104 | print(f"{key}: {details['value']} {details['unit'] if details['unit'] else ''}") 105 | last_data = victron_data.copy() # Aktualisiere den letzten Zustand der Daten 106 | 107 | # 5-Sekunden-Pause nach jedem Lesevorgang 108 | time.sleep(5) 109 | except KeyboardInterrupt: 110 | print("Programm beendet.") 111 | finally: 112 | ser.close() 113 | print("Serielle Verbindung geschlossen.") 114 | -------------------------------------------------------------------------------- /custom_components/onewire_sysbus/sensor.py: -------------------------------------------------------------------------------- 1 | """Support for 1-Wire environment sensors.""" 2 | from __future__ import annotations 3 | 4 | import asyncio 5 | from collections.abc import Callable, Mapping 6 | from dataclasses import dataclass 7 | import logging 8 | from typing import Any 9 | 10 | from pi1wire import InvalidCRCException, OneWireInterface, UnsupportResponseException 11 | 12 | from homeassistant.components.sensor import ( 13 | SensorDeviceClass, 14 | SensorEntity, 15 | SensorEntityDescription, 16 | SensorStateClass, 17 | ) 18 | from homeassistant.config_entries import ConfigEntry 19 | from homeassistant.const import UnitOfTemperature 20 | from homeassistant.core import HomeAssistant 21 | from homeassistant.helpers.entity import DeviceInfo 22 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 23 | from homeassistant.helpers.typing import StateType 24 | 25 | from .const import DOMAIN, READ_MODE_FLOAT 26 | from .model import OWDeviceDescription 27 | from .onewire_entities import OneWireEntity, OneWireEntityDescription 28 | from .onewirehub import OneWireHub 29 | 30 | 31 | @dataclass 32 | class OneWireSensorEntityDescription(OneWireEntityDescription, SensorEntityDescription): 33 | """Class describing OneWire sensor entities.""" 34 | 35 | override_key: Callable[[str, Mapping[str, Any]], str] | None = None 36 | 37 | 38 | SIMPLE_TEMPERATURE_SENSOR_DESCRIPTION = OneWireSensorEntityDescription( 39 | key="temperature", 40 | device_class=SensorDeviceClass.TEMPERATURE, 41 | name="Temperature", 42 | native_unit_of_measurement=UnitOfTemperature.CELSIUS, 43 | read_mode=READ_MODE_FLOAT, 44 | state_class=SensorStateClass.MEASUREMENT, 45 | ) 46 | 47 | _LOGGER = logging.getLogger(__name__) 48 | 49 | 50 | async def async_setup_entry( 51 | hass: HomeAssistant, 52 | config_entry: ConfigEntry, 53 | async_add_entities: AddEntitiesCallback, 54 | ) -> None: 55 | """Set up 1-Wire platform.""" 56 | onewirehub = hass.data[DOMAIN][config_entry.entry_id] 57 | entities = await hass.async_add_executor_job(get_entities, onewirehub) 58 | async_add_entities(entities, True) 59 | 60 | 61 | def get_entities(onewirehub: OneWireHub) -> list[SensorEntity]: 62 | """Get a list of entities.""" 63 | if not onewirehub.devices: 64 | return [] 65 | 66 | entities: list[SensorEntity] = [] 67 | 68 | for device in onewirehub.devices: 69 | assert isinstance(device, OWDeviceDescription) 70 | p1sensor: OneWireInterface = device.interface 71 | family = p1sensor.mac_address[:2] 72 | device_id = f"{family}-{p1sensor.mac_address[2:]}" 73 | device_info = device.device_info 74 | description = SIMPLE_TEMPERATURE_SENSOR_DESCRIPTION 75 | device_file = f"/sys/bus/w1/devices/{device_id}/w1_slave" 76 | name = f"{device_id} {description.name}" 77 | entities.append( 78 | OneWireSensor( 79 | description=description, 80 | device_id=device_id, 81 | device_file=device_file, 82 | device_info=device_info, 83 | name=name, 84 | owsensor=p1sensor, 85 | ) 86 | ) 87 | 88 | return entities 89 | 90 | 91 | class OneWireSensor(OneWireEntity, SensorEntity): 92 | """Implementation of a 1-Wire sensor directly connected to RPI GPIO.""" 93 | 94 | entity_description: OneWireSensorEntityDescription 95 | 96 | def __init__( 97 | self, 98 | description: OneWireSensorEntityDescription, 99 | device_id: str, 100 | device_info: DeviceInfo, 101 | device_file: str, 102 | name: str, 103 | owsensor: OneWireInterface, 104 | ) -> None: 105 | """Initialize the sensor.""" 106 | super().__init__( 107 | description=description, 108 | device_id=device_id, 109 | device_info=device_info, 110 | device_file=device_file, 111 | name=name, 112 | ) 113 | self._attr_unique_id = device_file 114 | self._owsensor = owsensor 115 | 116 | @property 117 | def native_value(self) -> StateType: 118 | """Return the state of the entity.""" 119 | return self._state 120 | 121 | async def get_temperature(self) -> float: 122 | """Get the latest data from the device.""" 123 | attempts = 1 124 | while True: 125 | try: 126 | return await self.hass.async_add_executor_job( 127 | self._owsensor.get_temperature 128 | ) 129 | except UnsupportResponseException as ex: 130 | _LOGGER.debug( 131 | "Cannot read from sensor %s (retry attempt %s): %s", 132 | self._device_file, 133 | attempts, 134 | ex, 135 | ) 136 | await asyncio.sleep(0.2) 137 | attempts += 1 138 | if attempts > 10: 139 | raise 140 | 141 | async def async_update(self) -> None: 142 | """Get the latest data from the device.""" 143 | try: 144 | self._value_raw = await self.get_temperature() 145 | self._state = round(self._value_raw, 1) 146 | except ( 147 | FileNotFoundError, 148 | InvalidCRCException, 149 | UnsupportResponseException, 150 | ) as ex: 151 | _LOGGER.warning( 152 | "Cannot read from sensor %s: %s", 153 | self._device_file, 154 | ex, 155 | ) 156 | self._state = None 157 | -------------------------------------------------------------------------------- /custom_components/mcp23017/config_flow.py: -------------------------------------------------------------------------------- 1 | """Config flow for MCP23017 component.""" 2 | 3 | import voluptuous as vol 4 | 5 | from homeassistant import config_entries 6 | from homeassistant.core import callback 7 | 8 | from . import i2c_device_exist 9 | from .const import ( 10 | CONF_FLOW_PIN_NAME, 11 | CONF_FLOW_PIN_NUMBER, 12 | CONF_FLOW_PLATFORM, 13 | CONF_I2C_ADDRESS, 14 | CONF_INVERT_LOGIC, 15 | CONF_PULL_MODE, 16 | CONF_HW_SYNC, 17 | DEFAULT_I2C_ADDRESS, 18 | DEFAULT_INVERT_LOGIC, 19 | DEFAULT_PULL_MODE, 20 | DEFAULT_HW_SYNC, 21 | DOMAIN, 22 | MODE_DOWN, 23 | MODE_UP, 24 | ) 25 | 26 | PLATFORMS = ["binary_sensor", "switch"] 27 | 28 | 29 | class Mcp23017ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 30 | """MCP23017 config flow.""" 31 | 32 | VERSION = 1 33 | CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH 34 | 35 | def _title(self, user_input): 36 | return "0x%02x:pin %d ('%s':%s)" % ( 37 | user_input[CONF_I2C_ADDRESS], 38 | user_input[CONF_FLOW_PIN_NUMBER], 39 | user_input[CONF_FLOW_PIN_NAME], 40 | user_input[CONF_FLOW_PLATFORM], 41 | ) 42 | 43 | def _unique_id(self, user_input): 44 | return "%s.%d.%d" % ( 45 | DOMAIN, 46 | user_input[CONF_I2C_ADDRESS], 47 | user_input[CONF_FLOW_PIN_NUMBER], 48 | ) 49 | 50 | @staticmethod 51 | @callback 52 | def async_get_options_flow(config_entry): 53 | """Add support for config flow options.""" 54 | return Mcp23017OptionsFlowHandler(config_entry) 55 | 56 | async def async_step_import(self, user_input=None): 57 | """Create a new entity from configuration.yaml import.""" 58 | 59 | config_entry = await self.async_set_unique_id(self._unique_id(user_input)) 60 | # Remove entry (from storage) matching the same unique id 61 | if config_entry: 62 | await self.hass.config_entries.async_remove(config_entry.entry_id) 63 | 64 | return self.async_create_entry( 65 | title=self._title(user_input), 66 | data=user_input, 67 | ) 68 | 69 | 70 | async def async_step_user(self, user_input=None): 71 | """Create a new entity from UI.""" 72 | 73 | if user_input is not None: 74 | await self.async_set_unique_id(self._unique_id(user_input)) 75 | self._abort_if_unique_id_configured() 76 | 77 | if CONF_FLOW_PIN_NAME not in user_input: 78 | user_input[CONF_FLOW_PIN_NAME] = "pin 0x%02x:%d" % ( 79 | user_input[CONF_I2C_ADDRESS], 80 | user_input[CONF_FLOW_PIN_NUMBER], 81 | ) 82 | 83 | if i2c_device_exist(user_input[CONF_I2C_ADDRESS]): 84 | return self.async_create_entry( 85 | title=self._title(user_input), 86 | data=user_input, 87 | ) 88 | else: 89 | return self.async_abort(reason="Invalid I2C address") 90 | 91 | return self.async_show_form( 92 | step_id="user", 93 | data_schema=vol.Schema( 94 | { 95 | vol.Required( 96 | CONF_I2C_ADDRESS, default=DEFAULT_I2C_ADDRESS 97 | ): vol.All(vol.Coerce(int), vol.Range(min=0, max=127)), 98 | vol.Required(CONF_FLOW_PIN_NUMBER, default=0): vol.All( 99 | vol.Coerce(int), vol.Range(min=0, max=15) 100 | ), 101 | vol.Required( 102 | CONF_FLOW_PLATFORM, 103 | default=PLATFORMS[0], 104 | ): vol.In(PLATFORMS), 105 | vol.Optional(CONF_FLOW_PIN_NAME): str, 106 | } 107 | ), 108 | ) 109 | 110 | 111 | class Mcp23017OptionsFlowHandler(config_entries.OptionsFlow): 112 | """MCP23017 config flow options.""" 113 | 114 | def __init__(self, config_entry): 115 | """Initialize options flow.""" 116 | self.config_entry = config_entry 117 | 118 | async def async_step_init(self, user_input=None): 119 | """Manage entity options.""" 120 | 121 | if user_input is not None: 122 | 123 | return self.async_create_entry(title="", data=user_input) 124 | 125 | data_schema = vol.Schema( 126 | { 127 | vol.Optional( 128 | CONF_INVERT_LOGIC, 129 | default=self.config_entry.options.get( 130 | CONF_INVERT_LOGIC, DEFAULT_INVERT_LOGIC 131 | ), 132 | ): bool, 133 | } 134 | ) 135 | if self.config_entry.data[CONF_FLOW_PLATFORM] == "binary_sensor": 136 | data_schema = data_schema.extend( 137 | { 138 | vol.Optional( 139 | CONF_PULL_MODE, 140 | default=self.config_entry.options.get( 141 | CONF_PULL_MODE, DEFAULT_PULL_MODE 142 | ), 143 | ): vol.In([MODE_UP, MODE_DOWN]), 144 | } 145 | ) 146 | 147 | if self.config_entry.data[CONF_FLOW_PLATFORM] == "switch": 148 | data_schema = data_schema.extend( 149 | { 150 | vol.Optional( 151 | CONF_HW_SYNC, 152 | default=self.config_entry.options.get( 153 | CONF_HW_SYNC, DEFAULT_HW_SYNC 154 | ), 155 | ): bool, 156 | } 157 | ) 158 | return self.async_show_form(step_id="init", data_schema=data_schema) 159 | -------------------------------------------------------------------------------- /custom_components/victron_mppt/sensor.py: -------------------------------------------------------------------------------- 1 | import serial 2 | import asyncio 3 | from homeassistant.components.sensor import PLATFORM_SCHEMA 4 | from homeassistant.helpers.entity import Entity 5 | import homeassistant.helpers.config_validation as cv 6 | import voluptuous as vol 7 | from queue import Queue 8 | import logging 9 | 10 | # Logger für die Komponente konfigurieren 11 | _LOGGER = logging.getLogger(__name__) 12 | 13 | # Lade Konfiguration aus der config.py 14 | from .config import key_mapping 15 | 16 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 17 | vol.Required("port"): cv.string, 18 | vol.Optional("baudrate", default=19200): cv.positive_int, 19 | vol.Optional("sleeptime", default=5): cv.positive_int, 20 | }) 21 | 22 | # Warteschlange für die Datenübergabe zwischen Thread und Home Assistant 23 | data_queue = Queue() 24 | 25 | def isnumber(number): 26 | number = number[1:] if number[0] == "-" else number 27 | return number.isnumeric() 28 | 29 | 30 | async def serial_reader_async(port, baudrate): 31 | """Asynchrone Funktion, um Daten von der seriellen Schnittstelle zu lesen und in der Queue zu speichern.""" 32 | try: 33 | ser = serial.Serial(port, baudrate) 34 | current_data = {} 35 | while True: 36 | # Verwende 'utf-8' statt 'ascii' mit errors='ignore', um problematische Zeichen zu vermeiden 37 | line = await asyncio.to_thread(ser.readline) # Nicht blockierendes Lesen 38 | line = line.decode('utf-8', errors='ignore').strip() 39 | if line: 40 | key, value = parse_line(line) 41 | #_LOGGER.debug(f"New incoming data: {key} = {value}") 42 | if key and value and key in key_mapping: 43 | mapped_key = key_mapping[key]["name"] 44 | if isnumber(value): 45 | scalefactore = key_mapping[key].get("scale", 1) 46 | value = int(value) * scalefactore 47 | roundby = key_mapping[key].get("round", None) 48 | value = round(value, roundby) if roundby else value 49 | current_data[mapped_key] = value 50 | 51 | if key == "Checksum": # Annahme, dass dies das Ende eines Datensatzes ist 52 | if current_data: # Überprüfen, ob Daten vorliegen 53 | data_queue.queue.clear() 54 | for key, value in current_data.items(): 55 | _LOGGER.debug(f"Safe new data to data_queue: {key} = {value}") 56 | data_queue.put(current_data.copy()) 57 | current_data.clear() # Leere den aktuellen Datensatz für den nächsten Zyklus 58 | else: 59 | _LOGGER.debug("Checksum, but no new data") 60 | await asyncio.sleep(0.01) # Geringe Pause für Event-Loop 61 | except Exception as e: 62 | _LOGGER.error(f"Fehler beim Lesen der seriellen Daten: {e}", ) 63 | 64 | def parse_line(line): 65 | """Parst eine Zeile des VE.Direct-Protokolls.""" 66 | try: 67 | key, value = line.split('\t') 68 | return key, value 69 | except ValueError: 70 | return None, None 71 | 72 | async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): 73 | """Setup der Victron-Sensoren asynchron.""" 74 | port = config["port"] 75 | baudrate = config["baudrate"] 76 | sleeptime = config["sleeptime"] 77 | 78 | # Starte die asynchrone serielle Lesefunktion 79 | hass.loop.create_task(serial_reader_async(port, baudrate)) 80 | _LOGGER.info("Asynchrone serielle Leser-Schleife gestartet.") 81 | 82 | # Initialisiere alle Entitäten für jede Konfiguration in key_mapping 83 | sensors = [VictronSensor(key, details) for key, details in key_mapping.items()] 84 | async_add_entities(sensors) 85 | _LOGGER.info("Victron-Sensoren wurden initialisiert und hinzugefügt.") 86 | 87 | # Starte die wiederholte Queue-Überwachung nur, wenn Daten verfügbar sind 88 | hass.loop.create_task(check_queue_and_update_sensors(sensors, sleeptime)) 89 | 90 | async def check_queue_and_update_sensors(sensors, sleeptime): 91 | """Wiederholte Aufgabe, um die Queue zu überprüfen und Sensoren zu aktualisieren.""" 92 | while True: 93 | if not data_queue.empty(): 94 | _LOGGER.debug("New data to HA") 95 | data = data_queue.get() 96 | for sensor in sensors: 97 | if sensor.name in data: 98 | _LOGGER.debug(f"Save to ha: {sensor.name}={data[sensor.name]}") 99 | sensor.update_state(data[sensor.name]) 100 | else: 101 | _LOGGER.debug(f"Not saved, as not found in data: {sensor.name}") 102 | 103 | 104 | await asyncio.sleep(sleeptime) # Pausiert für mehr Effizienz 105 | 106 | class VictronSensor(Entity): 107 | """Repräsentiert einen Victron-Sensor.""" 108 | 109 | def __init__(self, key, details): 110 | """Initialisiert den Sensor.""" 111 | self._key = key 112 | self._name = details["name"] 113 | self._unit = details["unit"] 114 | self._icon = details["icon"] 115 | self._state = None 116 | self._attr_should_poll = False # Schaltet Polling ab, da die Queue überwacht wird 117 | self._attr_available = True 118 | self._attr_unique_id = f"victron_{self._key}" 119 | 120 | @property 121 | def unique_id(self): 122 | """Gibt die eindeutige ID der Entität zurück.""" 123 | return self._attr_unique_id 124 | 125 | @property 126 | def name(self): 127 | """Gibt den Namen des Sensors zurück.""" 128 | return self._name 129 | 130 | @property 131 | def state(self): 132 | """Gibt den aktuellen Zustand des Sensors zurück.""" 133 | return self._state 134 | 135 | @property 136 | def unit_of_measurement(self): 137 | """Gibt die Maßeinheit des Sensors zurück.""" 138 | return self._unit 139 | 140 | @property 141 | def icon(self): 142 | """Gibt das Icon des Sensors zurück.""" 143 | return self._icon 144 | 145 | def update_state(self, value): 146 | """Aktualisiert den Zustand des Sensors nur, wenn sich der Wert geändert hat.""" 147 | if value != self._state: # Nur aktualisieren, wenn sich der Wert geändert hat 148 | self._state = value 149 | _LOGGER.debug(f"{self._name} Zustand aktualisiert: Neuer Wert = {self._state}") 150 | self.async_write_ha_state() # Asynchrones Schreiben in Home Assistant 151 | -------------------------------------------------------------------------------- /custom_components/victron_smartshunt/sensor.py: -------------------------------------------------------------------------------- 1 | import serial 2 | import asyncio 3 | from homeassistant.components.sensor import PLATFORM_SCHEMA 4 | from homeassistant.helpers.entity import Entity 5 | import homeassistant.helpers.config_validation as cv 6 | import voluptuous as vol 7 | from queue import Queue 8 | import logging 9 | 10 | # Logger für die Komponente konfigurieren 11 | _LOGGER = logging.getLogger(__name__) 12 | 13 | # Lade Konfiguration aus der config.py 14 | from .config import key_mapping 15 | 16 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 17 | vol.Required("port"): cv.string, 18 | vol.Optional("baudrate", default=19200): cv.positive_int, 19 | vol.Optional("sleeptime", default=5): cv.positive_int, 20 | }) 21 | 22 | # Warteschlange für die Datenübergabe zwischen Thread und Home Assistant 23 | data_queue = Queue() 24 | 25 | def isnumber(number): 26 | number = number[1:] if number[0] == "-" else number 27 | return number.isnumeric() 28 | 29 | 30 | async def serial_reader_async(port, baudrate): 31 | """Asynchrone Funktion, um Daten von der seriellen Schnittstelle zu lesen und in der Queue zu speichern.""" 32 | try: 33 | ser = serial.Serial(port, baudrate) 34 | current_data = {} 35 | while True: 36 | # Verwende 'utf-8' statt 'ascii' mit errors='ignore', um problematische Zeichen zu vermeiden 37 | line = await asyncio.to_thread(ser.readline) # Nicht blockierendes Lesen 38 | line = line.decode('utf-8', errors='ignore').strip() 39 | if line: 40 | key, value = parse_line(line) 41 | #_LOGGER.debug(f"New incoming data: {key} = {value}") 42 | if key and value and key in key_mapping: 43 | mapped_key = key_mapping[key]["name"] 44 | if isnumber(value): 45 | scalefactore = key_mapping[key].get("scale", 1) 46 | value = int(value) * scalefactore 47 | roundby = key_mapping[key].get("round", None) 48 | value = round(value, roundby) if roundby else value 49 | current_data[mapped_key] = value 50 | 51 | if key == "Checksum": # Annahme, dass dies das Ende eines Datensatzes ist 52 | if current_data: # Überprüfen, ob Daten vorliegen 53 | data_queue.queue.clear() 54 | for key, value in current_data.items(): 55 | _LOGGER.debug(f"Safe new data to data_queue: {key} = {value}") 56 | data_queue.put(current_data.copy()) 57 | current_data.clear() # Leere den aktuellen Datensatz für den nächsten Zyklus 58 | else: 59 | _LOGGER.debug("Checksum, but no new data") 60 | await asyncio.sleep(0.01) # Geringe Pause für Event-Loop 61 | except Exception as e: 62 | _LOGGER.error(f"Fehler beim Lesen der seriellen Daten: {e}", ) 63 | 64 | def parse_line(line): 65 | """Parst eine Zeile des VE.Direct-Protokolls.""" 66 | try: 67 | key, value = line.split('\t') 68 | return key, value 69 | except ValueError: 70 | return None, None 71 | 72 | async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): 73 | """Setup der VSmart-Sensoren asynchron.""" 74 | port = config["port"] 75 | baudrate = config["baudrate"] 76 | sleeptime = config["sleeptime"] 77 | 78 | # Starte die asynchrone serielle Lesefunktion 79 | hass.loop.create_task(serial_reader_async(port, baudrate)) 80 | _LOGGER.info("Asynchrone serielle Leser-Schleife gestartet.") 81 | 82 | # Initialisiere alle Entitäten für jede Konfiguration in key_mapping 83 | sensors = [VSmartSensor(key, details) for key, details in key_mapping.items()] 84 | async_add_entities(sensors) 85 | _LOGGER.info("VSmart-Sensoren wurden initialisiert und hinzugefügt.") 86 | 87 | # Starte die wiederholte Queue-Überwachung nur, wenn Daten verfügbar sind 88 | hass.loop.create_task(check_queue_and_update_sensors(sensors, sleeptime)) 89 | 90 | async def check_queue_and_update_sensors(sensors, sleeptime): 91 | """Wiederholte Aufgabe, um die Queue zu überprüfen und Sensoren zu aktualisieren.""" 92 | while True: 93 | if not data_queue.empty(): 94 | _LOGGER.debug("New data to HA") 95 | data = data_queue.get() 96 | for sensor in sensors: 97 | if sensor.name in data: 98 | _LOGGER.debug(f"Save to ha: {sensor.name}={data[sensor.name]}") 99 | sensor.update_state(data[sensor.name]) 100 | else: 101 | _LOGGER.debug(f"Not saved, as not found in data: {sensor.name}") 102 | 103 | 104 | await asyncio.sleep(sleeptime) # Pausiert für mehr Effizienz 105 | 106 | class VSmartSensor(Entity): 107 | """Repräsentiert einen VSmart-Sensor.""" 108 | 109 | def __init__(self, key, details): 110 | """Initialisiert den Sensor.""" 111 | self._key = key 112 | self._name = details["name"] 113 | self._unit = details["unit"] 114 | self._icon = details["icon"] 115 | self._state = None 116 | self._attr_should_poll = False # Schaltet Polling ab, da die Queue überwacht wird 117 | self._attr_available = True 118 | self._attr_unique_id = f"vsmart_{self._key}" 119 | 120 | @property 121 | def unique_id(self): 122 | """Gibt die eindeutige ID der Entität zurück.""" 123 | return self._attr_unique_id 124 | 125 | @property 126 | def name(self): 127 | """Gibt den Namen des Sensors zurück.""" 128 | return self._name 129 | 130 | @property 131 | def state(self): 132 | """Gibt den aktuellen Zustand des Sensors zurück.""" 133 | return self._state 134 | 135 | @property 136 | def unit_of_measurement(self): 137 | """Gibt die Maßeinheit des Sensors zurück.""" 138 | return self._unit 139 | 140 | @property 141 | def icon(self): 142 | """Gibt das Icon des Sensors zurück.""" 143 | return self._icon 144 | 145 | def update_state(self, value): 146 | """Aktualisiert den Zustand des Sensors nur, wenn sich der Wert geändert hat.""" 147 | if value != self._state: # Nur aktualisieren, wenn sich der Wert geändert hat 148 | self._state = value 149 | _LOGGER.debug(f"{self._name} Zustand aktualisiert: Neuer Wert = {self._state}") 150 | self.async_write_ha_state() # Asynchrones Schreiben in Home Assistant 151 | -------------------------------------------------------------------------------- /custom_components/mcp23017/switch.py: -------------------------------------------------------------------------------- 1 | """Platform for mcp23017-based switch.""" 2 | 3 | import asyncio 4 | import functools 5 | import logging 6 | 7 | import voluptuous as vol 8 | 9 | from . import async_get_or_create, setup_entry_status 10 | from homeassistant.components.switch import PLATFORM_SCHEMA, ToggleEntity 11 | from homeassistant.helpers.device_registry import DeviceEntryType 12 | from homeassistant.config_entries import SOURCE_IMPORT 13 | from homeassistant.core import callback 14 | import homeassistant.helpers.config_validation as cv 15 | 16 | from .const import ( 17 | CONF_FLOW_PIN_NAME, 18 | CONF_FLOW_PIN_NUMBER, 19 | CONF_FLOW_PLATFORM, 20 | CONF_I2C_ADDRESS, 21 | CONF_INVERT_LOGIC, 22 | CONF_HW_SYNC, 23 | CONF_PINS, 24 | DEFAULT_I2C_ADDRESS, 25 | DEFAULT_INVERT_LOGIC, 26 | DEFAULT_HW_SYNC, 27 | DOMAIN, 28 | ) 29 | 30 | _LOGGER = logging.getLogger(__name__) 31 | 32 | _SWITCHES_SCHEMA = vol.Schema({cv.positive_int: cv.string}) 33 | 34 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( 35 | { 36 | vol.Required(CONF_PINS): _SWITCHES_SCHEMA, 37 | vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, 38 | vol.Optional(CONF_HW_SYNC, default=DEFAULT_HW_SYNC): cv.boolean, 39 | vol.Optional(CONF_I2C_ADDRESS, default=DEFAULT_I2C_ADDRESS): vol.Coerce(int), 40 | } 41 | ) 42 | 43 | 44 | async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): 45 | """Set up the MCP23017 for switch entities.""" 46 | 47 | # Wait for configflow to terminate before processing configuration.yaml 48 | while setup_entry_status.busy(): 49 | await asyncio.sleep(0) 50 | 51 | for pin_number, pin_name in config[CONF_PINS].items(): 52 | hass.async_create_task( 53 | hass.config_entries.flow.async_init( 54 | DOMAIN, 55 | context={"source": SOURCE_IMPORT}, 56 | data={ 57 | CONF_FLOW_PLATFORM: "switch", 58 | CONF_FLOW_PIN_NUMBER: pin_number, 59 | CONF_FLOW_PIN_NAME: pin_name, 60 | CONF_I2C_ADDRESS: config[CONF_I2C_ADDRESS], 61 | CONF_INVERT_LOGIC: config[CONF_INVERT_LOGIC], 62 | CONF_HW_SYNC: config[CONF_HW_SYNC], 63 | }, 64 | ) 65 | ) 66 | 67 | 68 | async def async_setup_entry(hass, config_entry, async_add_entities): 69 | """Set up a MCP23017 switch entry.""" 70 | 71 | switch_entity = MCP23017Switch(hass, config_entry) 72 | switch_entity.device = await async_get_or_create( 73 | hass, config_entry, switch_entity 74 | ) 75 | 76 | if await hass.async_add_executor_job(switch_entity.configure_device): 77 | async_add_entities([switch_entity]) 78 | 79 | 80 | async def async_unload_entry(hass, config_entry): 81 | """Unload MCP23017 switch entry corresponding to config_entry.""" 82 | _LOGGER.warning("[FIXME] async_unload_entry not implemented") 83 | 84 | 85 | class MCP23017Switch(ToggleEntity): 86 | """Represent a switch that uses MCP23017.""" 87 | 88 | def __init__(self, hass, config_entry): 89 | """Initialize the MCP23017 switch.""" 90 | self._device = None 91 | self._state = None 92 | 93 | self._i2c_address = config_entry.data[CONF_I2C_ADDRESS] 94 | self._pin_name = config_entry.data[CONF_FLOW_PIN_NAME] 95 | self._pin_number = config_entry.data[CONF_FLOW_PIN_NUMBER] 96 | 97 | # Get invert_logic from config flow (options) or import (data) 98 | self._invert_logic = config_entry.options.get( 99 | CONF_INVERT_LOGIC, 100 | config_entry.data.get( 101 | CONF_INVERT_LOGIC, 102 | DEFAULT_INVERT_LOGIC 103 | ) 104 | ) 105 | 106 | # Get hw_sync from config flow (options) or import (data) 107 | self._hw_sync = config_entry.options.get( 108 | CONF_HW_SYNC, 109 | config_entry.data.get( 110 | CONF_HW_SYNC, 111 | DEFAULT_HW_SYNC 112 | ) 113 | ) 114 | 115 | # Create or update option values for switch platform 116 | hass.config_entries.async_update_entry( 117 | config_entry, 118 | options={ 119 | CONF_INVERT_LOGIC: self._invert_logic, 120 | CONF_HW_SYNC: self._hw_sync, 121 | }, 122 | ) 123 | 124 | # Subscribe to updates of config entry options 125 | self._unsubscribe_update_listener = config_entry.add_update_listener( 126 | self.async_config_update 127 | ) 128 | 129 | _LOGGER.info( 130 | "%s(pin %d:'%s') created", 131 | type(self).__name__, 132 | self._pin_number, 133 | self._pin_name, 134 | ) 135 | 136 | @property 137 | def icon(self): 138 | """Return device icon for this entity.""" 139 | return "mdi:chip" 140 | 141 | @property 142 | def unique_id(self): 143 | """Return a unique_id for this entity.""" 144 | return f"{self._device.unique_id}-0x{self._pin_number:02x}" 145 | 146 | @property 147 | def name(self): 148 | """Return the name of the switch.""" 149 | return self._pin_name 150 | 151 | @property 152 | def is_on(self): 153 | """Return true if device is on.""" 154 | return self._state 155 | 156 | @property 157 | def pin(self): 158 | """Return the pin number of the entity.""" 159 | return self._pin_number 160 | 161 | @property 162 | def address(self): 163 | """Return the i2c address of the entity.""" 164 | return self._i2c_address 165 | 166 | @property 167 | def device_info(self): 168 | """Device info.""" 169 | return { 170 | "identifiers": {(DOMAIN, self._i2c_address)}, 171 | "manufacturer": "Microchip", 172 | "model": "MCP23017", 173 | "entry_type": DeviceEntryType.SERVICE, 174 | } 175 | 176 | @property 177 | def device(self): 178 | """Get device property.""" 179 | return self._device 180 | 181 | @device.setter 182 | def device(self, value): 183 | """Set device property.""" 184 | self._device = value 185 | 186 | async def async_turn_on(self, **kwargs): 187 | """Turn the device on.""" 188 | await self.hass.async_add_executor_job( 189 | functools.partial( 190 | self._device.set_pin_value, self._pin_number, not self._invert_logic 191 | ) 192 | ) 193 | self._state = True 194 | self.schedule_update_ha_state() 195 | 196 | async def async_turn_off(self, **kwargs): 197 | """Turn the device off.""" 198 | await self.hass.async_add_executor_job( 199 | functools.partial( 200 | self._device.set_pin_value, self._pin_number, self._invert_logic 201 | ) 202 | ) 203 | self._state = False 204 | self.schedule_update_ha_state() 205 | 206 | @callback 207 | async def async_config_update(self, hass, config_entry): 208 | """Handle update from config entry options.""" 209 | self._invert_logic = config_entry.options[CONF_INVERT_LOGIC] 210 | await hass.async_add_executor_job( 211 | functools.partial( 212 | self._device.set_pin_value, 213 | self._pin_number, 214 | self._state ^ self._invert_logic, 215 | ) 216 | ) 217 | self.async_schedule_update_ha_state() 218 | 219 | def unsubscribe_update_listener(self): 220 | """Remove listener from config entry options.""" 221 | self._unsubscribe_update_listener() 222 | 223 | # Sync functions executed outside of hass async loop. 224 | 225 | def configure_device(self): 226 | """Attach instance to a device on the given address and configure it. 227 | 228 | This function should be called from the thread pool as it contains blocking functions. 229 | 230 | Return True when successful. 231 | """ 232 | if self.device: 233 | # Reset pin value when HW sync is not required 234 | if not self._hw_sync: 235 | self._device.set_pin_value(self._pin_number, self._invert_logic) 236 | # Configure entity as output for a switch 237 | self._device.set_input(self._pin_number, False) 238 | self._state = self._device.get_pin_value(self._pin_number) ^ self._invert_logic 239 | 240 | return True 241 | 242 | return False 243 | -------------------------------------------------------------------------------- /custom_components/mcp23017/binary_sensor.py: -------------------------------------------------------------------------------- 1 | """Platform for mcp23017-based binary_sensor.""" 2 | 3 | import asyncio 4 | import functools 5 | import logging 6 | 7 | import voluptuous as vol 8 | 9 | from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity 10 | from homeassistant.helpers.device_registry import DeviceEntryType 11 | from . import async_get_or_create, setup_entry_status 12 | from homeassistant.config_entries import SOURCE_IMPORT 13 | from homeassistant.core import callback 14 | import homeassistant.helpers.config_validation as cv 15 | 16 | from .const import ( 17 | CONF_FLOW_PIN_NAME, 18 | CONF_FLOW_PIN_NUMBER, 19 | CONF_FLOW_PLATFORM, 20 | CONF_I2C_ADDRESS, 21 | CONF_INVERT_LOGIC, 22 | CONF_PINS, 23 | CONF_PULL_MODE, 24 | DEFAULT_I2C_ADDRESS, 25 | DEFAULT_INVERT_LOGIC, 26 | DEFAULT_PULL_MODE, 27 | DOMAIN, 28 | MODE_DOWN, 29 | MODE_UP, 30 | ) 31 | 32 | _LOGGER = logging.getLogger(__name__) 33 | 34 | _PIN_SCHEMA = vol.Schema({cv.positive_int: cv.string}) 35 | 36 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( 37 | { 38 | vol.Required(CONF_PINS): _PIN_SCHEMA, 39 | vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, 40 | vol.Optional(CONF_PULL_MODE, default=DEFAULT_PULL_MODE): vol.All( 41 | vol.Upper, vol.In([MODE_UP, MODE_DOWN]) 42 | ), 43 | vol.Optional(CONF_I2C_ADDRESS, default=DEFAULT_I2C_ADDRESS): vol.Coerce(int), 44 | } 45 | ) 46 | 47 | 48 | async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): 49 | """Set up the MCP23017 platform for binary_sensor entities.""" 50 | 51 | # Wait for configflow to terminate before processing configuration.yaml 52 | while setup_entry_status.busy(): 53 | await asyncio.sleep(0) 54 | 55 | for pin_number, pin_name in config[CONF_PINS].items(): 56 | hass.async_create_task( 57 | hass.config_entries.flow.async_init( 58 | DOMAIN, 59 | context={"source": SOURCE_IMPORT}, 60 | data={ 61 | CONF_FLOW_PLATFORM: "binary_sensor", 62 | CONF_FLOW_PIN_NUMBER: pin_number, 63 | CONF_FLOW_PIN_NAME: pin_name, 64 | CONF_I2C_ADDRESS: config[CONF_I2C_ADDRESS], 65 | CONF_INVERT_LOGIC: config[CONF_INVERT_LOGIC], 66 | CONF_PULL_MODE: config[CONF_PULL_MODE], 67 | }, 68 | ) 69 | ) 70 | 71 | 72 | async def async_setup_entry(hass, config_entry, async_add_entities): 73 | """Set up a MCP23017 binary_sensor entry.""" 74 | 75 | binary_sensor_entity = MCP23017BinarySensor(hass, config_entry) 76 | binary_sensor_entity.device = await async_get_or_create( 77 | hass, config_entry, binary_sensor_entity 78 | ) 79 | 80 | if await hass.async_add_executor_job(binary_sensor_entity.configure_device): 81 | async_add_entities([binary_sensor_entity]) 82 | 83 | 84 | async def async_unload_entry(hass, config_entry): 85 | """Unload MCP23017 binary_sensor entry corresponding to config_entry.""" 86 | _LOGGER.warning("[FIXME] async_unload_entry not implemented") 87 | 88 | 89 | class MCP23017BinarySensor(BinarySensorEntity): 90 | """Represent a binary sensor that uses MCP23017.""" 91 | 92 | def __init__(self, hass, config_entry): 93 | """Initialize the MCP23017 binary sensor.""" 94 | self._state = None 95 | self._device = None 96 | 97 | self._i2c_address = config_entry.data[CONF_I2C_ADDRESS] 98 | self._pin_name = config_entry.data[CONF_FLOW_PIN_NAME] 99 | self._pin_number = config_entry.data[CONF_FLOW_PIN_NUMBER] 100 | 101 | # Get invert_logic from config flow (options) or import (data) 102 | self._invert_logic = config_entry.options.get( 103 | CONF_INVERT_LOGIC, 104 | config_entry.data.get( 105 | CONF_INVERT_LOGIC, 106 | DEFAULT_INVERT_LOGIC 107 | ) 108 | ) 109 | # Get pull_mode from config flow (options) or import (data) 110 | self._pull_mode = config_entry.options.get( 111 | CONF_PULL_MODE, 112 | config_entry.data.get( 113 | CONF_PULL_MODE, 114 | DEFAULT_PULL_MODE 115 | ) 116 | ) 117 | 118 | # Create or update option values for binary_sensor platform 119 | hass.config_entries.async_update_entry( 120 | config_entry, 121 | options={ 122 | CONF_INVERT_LOGIC: self._invert_logic, 123 | CONF_PULL_MODE: self._pull_mode, 124 | }, 125 | ) 126 | 127 | # Subscribe to updates of config entry options. 128 | self._unsubscribe_update_listener = config_entry.add_update_listener( 129 | self.async_config_update 130 | ) 131 | 132 | _LOGGER.info( 133 | "%s(pin %d:'%s') created", 134 | type(self).__name__, 135 | self._pin_number, 136 | self._pin_name, 137 | ) 138 | 139 | @property 140 | def icon(self): 141 | """Return device icon for this entity.""" 142 | return "mdi:chip" 143 | 144 | @property 145 | def unique_id(self): 146 | """Return a unique_id for this entity.""" 147 | return f"{self._device.unique_id}-0x{self._pin_number:02x}" 148 | 149 | @property 150 | def should_poll(self): 151 | """No polling needed from homeassistant for this entity.""" 152 | return False 153 | 154 | @property 155 | def name(self): 156 | """Return the name of the entity.""" 157 | return self._pin_name 158 | 159 | @property 160 | def is_on(self): 161 | """Return the state of the entity.""" 162 | return self._state != self._invert_logic 163 | 164 | @property 165 | def pin(self): 166 | """Return the pin number of the entity.""" 167 | return self._pin_number 168 | 169 | @property 170 | def address(self): 171 | """Return the i2c address of the entity.""" 172 | return self._i2c_address 173 | 174 | @property 175 | def device_info(self): 176 | """Device info.""" 177 | return { 178 | "identifiers": {(DOMAIN, self._i2c_address)}, 179 | "manufacturer": "Microchip", 180 | "model": "MCP23017", 181 | "entry_type": DeviceEntryType.SERVICE, 182 | } 183 | 184 | @property 185 | def device(self): 186 | """Get device property.""" 187 | return self._device 188 | 189 | @device.setter 190 | def device(self, value): 191 | """Set device property.""" 192 | self._device = value 193 | 194 | @callback 195 | async def async_push_update(self, state): 196 | """Update the GPIO state.""" 197 | self._state = state 198 | self.async_schedule_update_ha_state() 199 | 200 | @callback 201 | async def async_config_update(self, hass, config_entry): 202 | """Handle update from config entry options.""" 203 | self._invert_logic = config_entry.options[CONF_INVERT_LOGIC] 204 | if self._pull_mode != config_entry.options[CONF_PULL_MODE]: 205 | self._pull_mode = config_entry.options[CONF_PULL_MODE] 206 | await hass.async_add_executor_job( 207 | functools.partial( 208 | self._device.set_pullup, 209 | self._pin_number, 210 | bool(self._pull_mode == MODE_UP), 211 | ) 212 | ) 213 | self.async_schedule_update_ha_state() 214 | 215 | def unsubscribe_update_listener(self): 216 | """Remove listener from config entry options.""" 217 | self._unsubscribe_update_listener() 218 | 219 | # Sync functions executed outside of the hass async loop 220 | 221 | def push_update(self, state): 222 | """Signal a state change and call the async counterpart.""" 223 | asyncio.run_coroutine_threadsafe(self.async_push_update(state), self.hass.loop) 224 | 225 | def configure_device(self): 226 | """Attach instance to a device on the given address and configure it. 227 | 228 | This function should be called from the thread pool as it contains blocking functions. 229 | 230 | Return True when successful. 231 | """ 232 | if self.device: 233 | # Configure entity as input for a binary sensor 234 | self._device.set_input(self._pin_number, True) 235 | self._device.set_pullup(self._pin_number, bool(self._pull_mode == MODE_UP)) 236 | 237 | return True 238 | 239 | return False 240 | -------------------------------------------------------------------------------- /custom_components/mpu650/sensor.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import math 3 | import time 4 | import threading 5 | import json 6 | import os 7 | import asyncio 8 | from concurrent.futures import ThreadPoolExecutor 9 | from collections import deque 10 | from homeassistant.components.sensor import SensorEntity 11 | from smbus2 import SMBus 12 | from homeassistant.helpers.event import async_track_state_change_event 13 | 14 | _LOGGER = logging.getLogger(__name__) 15 | 16 | # MPU6050-Register 17 | MPU6050_ADDR = 0x69 18 | MPU6050_PWR_MGMT_1 = 0x6B 19 | MPU6050_ACCEL_XOUT_H = 0x3B 20 | MPU6050_ACCEL_YOUT_H = 0x3D 21 | MPU6050_GYRO_XOUT_H = 0x43 22 | MPU6050_GYRO_YOUT_H = 0x45 23 | 24 | CALIBRATION_FILE = os.path.join(os.path.dirname(__file__), "mpu6050_calibration.json") 25 | bus = SMBus(1) 26 | 27 | class MPU6050AngleSensor(SensorEntity): 28 | def __init__(self, name, sensor_type): 29 | self._name = name 30 | self._sensor_type = sensor_type 31 | self._state = None 32 | self._attr_force_update = True 33 | self._attr_should_poll = True 34 | _LOGGER.debug(f"MPU6050AngleSensor {name} initialisiert.") 35 | 36 | @property 37 | def name(self): 38 | return self._name 39 | 40 | @property 41 | def state(self): 42 | return self._state 43 | 44 | @property 45 | def unique_id(self): 46 | return f"mpu6050_{self._sensor_type}" 47 | 48 | def update_state(self, value): 49 | self._state = round(value, 2) 50 | _LOGGER.debug(f"{self._sensor_type} Zustand auf {self._state} aktualisiert.") 51 | self.hass.add_job(self.async_write_ha_state) 52 | 53 | class MPU6050SensorManager: 54 | def __init__(self, hass, sensors, target_interval=5): 55 | self.hass = hass 56 | self.sensors = sensors 57 | self._stop_event = threading.Event() 58 | self._thread = None 59 | self.target_interval = target_interval 60 | 61 | self.accel_x_window = deque(maxlen=20) 62 | self.accel_y_window = deque(maxlen=20) 63 | 64 | # Kalibrierungsdaten asynchron laden 65 | loop = asyncio.get_running_loop() 66 | loop.create_task(self.load_calibration_async()) 67 | 68 | # Registrieren des Listeners für den Switch-Zustand 69 | async_track_state_change_event( 70 | self.hass, "switch.schalte_ausrichtung_ein", self.switch_listener 71 | ) 72 | 73 | _LOGGER.info("MPU6050SensorManager initialisiert.") 74 | switch_state = hass.states.get("switch.schalte_ausrichtung_ein") 75 | if switch_state and switch_state.state == "on": 76 | self.start() 77 | 78 | async def load_calibration_async(self): 79 | def load_calibration(): 80 | if os.path.exists(CALIBRATION_FILE): 81 | try: 82 | with open(CALIBRATION_FILE, "r") as file: 83 | calibration_data = json.load(file) 84 | return calibration_data.get("x_offset", 0.0), calibration_data.get("y_offset", 0.0) 85 | except Exception as e: 86 | _LOGGER.error(f"Fehler beim Laden der Kalibrierungsdaten: {e}") 87 | _LOGGER.info("Keine gespeicherten Kalibrierungsdaten gefunden. Standardwerte werden verwendet.") 88 | return 0.0, 0.0 89 | 90 | loop = asyncio.get_running_loop() 91 | x_offset, y_offset = await loop.run_in_executor(None, load_calibration) 92 | self.x_offset, self.y_offset = x_offset, y_offset 93 | _LOGGER.info("Kalibrierungsdaten erfolgreich geladen.") 94 | 95 | def switch_listener(self, event): 96 | new_state = event.data.get("new_state") 97 | if new_state and new_state.state == "on": 98 | self.start() 99 | elif new_state and new_state.state == "off": 100 | self.stop() 101 | 102 | def start(self): 103 | if self._thread is None or not self._thread.is_alive(): 104 | self._stop_event.clear() 105 | self._thread = threading.Thread(target=self.read_sensor_data) 106 | self._thread.start() 107 | _LOGGER.info("MPU6050SensorManager gestartet.") 108 | else: 109 | _LOGGER.warning("Datenlese-Thread läuft bereits.") 110 | _LOGGER.warning(self._thread.is_alive()) 111 | 112 | def stop(self): 113 | if self._thread is not None and self._thread.is_alive(): 114 | self._stop_event.set() 115 | self._thread.join() 116 | self._thread = None 117 | _LOGGER.info("MPU6050SensorManager gestoppt.") 118 | 119 | def save_calibration(self, x_offset, y_offset): 120 | calibration_data = { 121 | "x_offset": x_offset, 122 | "y_offset": y_offset 123 | } 124 | try: 125 | with open(CALIBRATION_FILE, "w") as file: 126 | json.dump(calibration_data, file) 127 | _LOGGER.info("Kalibrierungsdaten erfolgreich gespeichert.") 128 | except Exception as e: 129 | _LOGGER.error(f"Fehler beim Speichern der Kalibrierungsdaten: {e}") 130 | 131 | def calibrate(self): 132 | try: 133 | _LOGGER.info("Kalibrierung wird gestartet...") 134 | accel_x_offset = 0 135 | accel_y_offset = 0 136 | num_samples = 300 137 | 138 | for _ in range(num_samples): 139 | accel_x_offset += read_raw_data(MPU6050_ACCEL_XOUT_H) 140 | accel_y_offset += read_raw_data(MPU6050_ACCEL_YOUT_H) 141 | time.sleep(0.01) 142 | 143 | accel_x_offset /= num_samples 144 | accel_y_offset /= num_samples 145 | 146 | self.save_calibration(accel_x_offset, accel_y_offset) 147 | 148 | # Korrekte Zuweisung der Offsets 149 | self.x_offset = accel_x_offset 150 | self.y_offset = accel_y_offset 151 | 152 | _LOGGER.info("Kalibrierung abgeschlossen. Offsets gespeichert.") 153 | except Exception as e: 154 | _LOGGER.error(f"Fehler bei der Kalibrierung: {e}") 155 | 156 | def read_sensor_data(self): 157 | try: 158 | bus.write_byte_data(MPU6050_ADDR, MPU6050_PWR_MGMT_1, 0) 159 | except Exception as e: 160 | _LOGGER.error(f"Fehler bei der Initialisierung des MPU6050: {e}") 161 | return 162 | 163 | angle_x, angle_y = 0.0, 0.0 164 | 165 | while not self._stop_event.is_set(): 166 | switch_state = self.hass.states.get("switch.schalte_ausrichtung_ein") 167 | if switch_state is None or switch_state.state == "off": 168 | _LOGGER.info("Schalter ist aus; Sensor-Updates sind pausiert.") 169 | self._stop_event.wait(5) 170 | continue 171 | 172 | start_time = time.time() 173 | try: 174 | accel_x = read_raw_data(MPU6050_ACCEL_XOUT_H) - self.x_offset 175 | accel_y = read_raw_data(MPU6050_ACCEL_YOUT_H) - self.y_offset 176 | #accel_z = read_raw_data(MPU6050_ACCEL_XOUT_H + 2) 177 | 178 | #gyro_x = read_raw_data(MPU6050_GYRO_XOUT_H) - self.x_offset 179 | #gyro_y = read_raw_data(MPU6050_GYRO_YOUT_H) - self.y_offset 180 | 181 | #accel_angle_x = math.atan2(accel_y, math.sqrt(accel_x**2 + accel_z**2)) * (180 / math.pi) 182 | #accel_angle_y = math.atan2(-accel_x, math.sqrt(accel_y**2 + accel_z**2)) * (180 / math.pi) 183 | 184 | #dt = time.time() - start_time 185 | angle_x = math.atan(accel_x / 16384.0) * (180 / math.pi) 186 | angle_y = math.atan(accel_y / 16384.0) * (180 / math.pi) 187 | 188 | 189 | for sensor in self.sensors: 190 | if sensor._sensor_type == "x_angle": 191 | sensor.update_state(angle_x) 192 | elif sensor._sensor_type == "y_angle": 193 | sensor.update_state(angle_y) 194 | 195 | time.sleep(max(0, self.target_interval - (time.time() - start_time))) 196 | except Exception as e: 197 | _LOGGER.error(f"Fehler beim Lesen der Sensordaten: {e}",stack_info=True, exc_info=True) 198 | break 199 | 200 | def read_raw_data(addr): 201 | try: 202 | high = bus.read_byte_data(MPU6050_ADDR, addr) 203 | low = bus.read_byte_data(MPU6050_ADDR, addr + 1) 204 | 205 | value = ((high << 8) | low) 206 | if value > 32768: 207 | value = value - 65536 208 | return value 209 | except Exception as e: 210 | _LOGGER.error(f"Fehler beim Lesen von Rohdaten von {addr}: {e}") 211 | return 0 212 | 213 | async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): 214 | sensors = [ 215 | MPU6050AngleSensor("MPU6050 X Winkel", "x_angle"), 216 | MPU6050AngleSensor("MPU6050 Y Winkel", "y_angle") 217 | ] 218 | 219 | manager = MPU6050SensorManager(hass, sensors, target_interval=5) 220 | hass.data["mpu6050_sensor_manager"] = manager 221 | async_add_entities(sensors) 222 | 223 | _LOGGER.info("MPU6050 Sensor-Plattform erfolgreich eingerichtet.") 224 | -------------------------------------------------------------------------------- /custom_components/ads_waterlevel/sensor.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from datetime import timedelta 4 | import json 5 | import aiofiles 6 | import logging 7 | import os 8 | import time 9 | from typing import List, Tuple, Optional, Dict, Any 10 | 11 | from homeassistant.components.sensor import SensorEntity 12 | from homeassistant.const import UnitOfElectricPotential, UnitOfVolume 13 | from homeassistant.helpers.entity import DeviceInfo 14 | from smbus2 import SMBus 15 | 16 | _LOGGER = logging.getLogger(__name__) 17 | SCAN_INTERVAL = timedelta(seconds=5) 18 | 19 | # ---- ADS1115 --------------------------------------------------------------- 20 | ADS_ADDR_DEFAULT = 0x48 21 | REG_CONV = 0x00 22 | REG_CONFIG = 0x01 23 | 24 | # Single-ended MUX AINx vs GND 25 | MUX_MAP = {0: 0x4000, 1: 0x5000, 2: 0x6000, 3: 0x7000} 26 | 27 | # PGA ±4.096 V → passt gut für 0–3.3V Signale 28 | PGA_BITS = 0x0200 29 | PGA_RANGE_V = 4.096 30 | 31 | # Single-shot, 128 SPS, Comparator disabled 32 | OS_START = 0x8000 33 | MODE_SINGLE = 0x0100 34 | DR_128SPS = 0x0080 35 | COMP_DISABLE = 0x0003 36 | 37 | def _build_cfg(channel: int) -> int: 38 | return OS_START | MUX_MAP[channel] | PGA_BITS | MODE_SINGLE | DR_128SPS | COMP_DISABLE 39 | 40 | # ---- Hilfen ---------------------------------------------------------------- 41 | def ch_human_to_ain(ch: int) -> int: 42 | """ 43 | Erlaubt channel: 1..4 (Board-Beschriftung) oder 0..3 (AIN direkt). 44 | """ 45 | ch = int(ch) 46 | if 1 <= ch <= 4: 47 | return ch - 1 # 1→0, 2→1, 3→2, 4→3 48 | if 0 <= ch <= 3: 49 | return ch 50 | raise ValueError(f"Ungültiger channel: {ch}") 51 | 52 | def build_linear_mapping(v_max: float, invert: bool, steps: int = 10) -> List[Tuple[float, float]]: 53 | """ 54 | Baut eine lineare (V→L)-Kurve von 0..v_max → 0..100 L (oder invertiert). 55 | steps = Anzahl Intervalle (10 → 11 Stützpunkte). 56 | """ 57 | pts: List[Tuple[float, float]] = [] 58 | for i in range(steps + 1): 59 | v = round(v_max * i / steps, 3) 60 | l = round(100.0 * i / steps, 1) 61 | if invert: 62 | l = round(100.0 - l, 1) 63 | pts.append((v, l)) 64 | return pts 65 | 66 | def normalize_mapping_points(items: List[Dict[str, Any]], v_max: float, invert: bool) -> List[Tuple[float, float]]: 67 | """ 68 | Erwartet Liste von {v: , l: } und sortiert diese. 69 | Ergänzt ggf. (0→0/100) und (v_max→100/0). 70 | """ 71 | pts = [] 72 | for it in items: 73 | v = float(it["v"]); l = float(it["l"]) 74 | pts.append((v, l)) 75 | pts.sort(key=lambda x: x[0]) 76 | 77 | # Start/Ende erzwingen, falls nicht vorhanden 78 | have0 = any(abs(v) < 1e-6 for v, _ in pts) 79 | haveMax = any(abs(v - v_max) < 1e-6 for v, _ in pts) 80 | if not have0: 81 | pts = [ (0.0, 100.0 if invert else 0.0) ] + pts 82 | if not haveMax: 83 | pts = pts + [ (v_max, 0.0 if invert else 100.0) ] 84 | return pts 85 | 86 | def interp(points: List[Tuple[float, float]], x: float) -> float: 87 | """ 88 | Lineare Interpolation über sortierte (x→y)-Punkte. 89 | """ 90 | if not points: 91 | return 0.0 92 | if x <= points[0][0]: 93 | return points[0][1] 94 | if x >= points[-1][0]: 95 | return points[-1][1] 96 | for i in range(1, len(points)): 97 | x0, y0 = points[i-1]; x1, y1 = points[i] 98 | if x0 <= x <= x1: 99 | if x1 == x0: 100 | return y0 101 | t = (x - x0) / (x1 - x0) 102 | return y0 + t * (y1 - y0) 103 | return points[-1][1] 104 | 105 | # ---- Sensor-Entitäten ------------------------------------------------------ 106 | class ADSVoltageSensor(SensorEntity): 107 | _attr_device_class = "voltage" 108 | _attr_native_unit_of_measurement = UnitOfElectricPotential.VOLT 109 | _attr_state_class = "measurement" 110 | _attr_should_poll = True 111 | 112 | def __init__(self, base_name: str, ain: int, divider_ratio: float): 113 | self._base = base_name 114 | self._ain = ain 115 | self._ratio = float(divider_ratio or 1.0) 116 | self._bus = SMBus(1) 117 | self._addr = ADS_ADDR_DEFAULT 118 | self._last_vin: Optional[float] = None 119 | 120 | self._attr_name = f"{base_name} Voltage" 121 | self._attr_unique_id = f"ads_wl_v_{ain}_{base_name}" 122 | self._attr_device_info = DeviceInfo( 123 | identifiers={("ads_waterlevel", "ads1115")}, 124 | name="ADS1115 Water Level", 125 | manufacturer="Custom", 126 | model="ADS1115" 127 | ) 128 | 129 | def update(self) -> None: 130 | try: 131 | cfg = _build_cfg(self._ain) 132 | self._bus.write_i2c_block_data(self._addr, REG_CONFIG, [(cfg >> 8) & 0xFF, cfg & 0xFF]) 133 | time.sleep(0.009) # ~8 ms @128 SPS 134 | raw = self._bus.read_i2c_block_data(self._addr, REG_CONV, 2) 135 | val = (raw[0] << 8) | raw[1] 136 | if val > 0x7FFF: 137 | val -= 0x10000 # signed 16-bit 138 | 139 | v_adc = (val * PGA_RANGE_V) / 32768.0 140 | if v_adc < 0: v_adc = 0.0 141 | v_in = round(v_adc * self._ratio, 3) 142 | self._last_vin = v_in 143 | self._attr_native_value = round(v_in, 2) 144 | except Exception as e: 145 | _LOGGER.error("ADS1115 Read AIN%s fehlgeschlagen: %s", self._ain, e) 146 | self._attr_native_value = None 147 | self._last_vin = None 148 | 149 | def get_last_voltage(self) -> Optional[float]: 150 | return self._last_vin 151 | 152 | class ADSLevelSensor(SensorEntity): 153 | _attr_native_unit_of_measurement = UnitOfVolume.LITERS 154 | _attr_state_class = "measurement" 155 | _attr_should_poll = True 156 | 157 | def __init__(self, base_name: str, voltage_sensor: ADSVoltageSensor, 158 | mapping_points: List[Tuple[float, float]]): 159 | self._base = base_name 160 | self._vs = voltage_sensor 161 | self._map = mapping_points 162 | self._attr_name = f"{base_name} Level" 163 | self._attr_unique_id = f"ads_wl_l_{base_name}" 164 | 165 | def update(self) -> None: 166 | v = self._vs.get_last_voltage() 167 | if v is None: 168 | self._vs.update() 169 | v = self._vs.get_last_voltage() 170 | if v is None: 171 | self._attr_native_value = None 172 | return 173 | liters = interp(self._map, v) 174 | self._attr_native_value = round(liters, 1) 175 | 176 | class ADSResistanceSensor(SensorEntity): 177 | """Nur bei mode: resistive – R aus Pull-Up berechnen.""" 178 | _attr_native_unit_of_measurement = "Ω" 179 | _attr_state_class = "measurement" 180 | _attr_should_poll = True 181 | 182 | def __init__(self, base_name: str, voltage_sensor: ADSVoltageSensor, r_pullup: float, v_ref: float): 183 | self._base = base_name 184 | self._vs = voltage_sensor 185 | self._rpu = float(r_pullup or 47000.0) 186 | self._vref = float(v_ref or 3.3) 187 | self._attr_name = f"{base_name} Resistance" 188 | self._attr_unique_id = f"ads_wl_r_{base_name}" 189 | 190 | def update(self) -> None: 191 | v = self._vs.get_last_voltage() 192 | if v is None: 193 | self._vs.update() 194 | v = self._vs.get_last_voltage() 195 | if v is None or v >= self._vref: 196 | self._attr_native_value = None 197 | return 198 | r = self._rpu * (v / (self._vref - v)) 199 | self._attr_native_value = round(r, 0) 200 | 201 | # ---- Plattform-Setup ------------------------------------------------------- 202 | async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): 203 | entities: List[SensorEntity] = [] 204 | 205 | # Pair-Defaults (pro „1-2“ & „3-4“) 206 | pair_cfg: Dict[str, Dict[str, Any]] = config.get("pair_config", {}) 207 | def defaults_for_channel_human(ch_human: int) -> Dict[str, Any]: 208 | pair_key = "1-2" if ch_human in (1,2) else "3-4" 209 | # Standardwerte: 210 | d = { 211 | "mode": "voltage", # voltage | capacitive | resistive 212 | "v_max": 3.3, 213 | "invert": False, 214 | # "r_pullup_ohm": 47000, "v_ref": 3.3 215 | } 216 | d.update(pair_cfg.get(pair_key, {})) 217 | return d 218 | 219 | for s in config.get("sensors", []): 220 | name = s["name"] 221 | ch_human = int(s["channel"]) # 1..4 erlaubt 222 | ain = ch_human_to_ain(ch_human) 223 | 224 | # Pair-Defaults + Sensor-Overrides zusammenführen 225 | dfl = defaults_for_channel_human(ch_human) 226 | mode = s.get("mode", dfl["mode"]) 227 | v_max = float(s.get("v_max", dfl.get("v_max", 3.3))) 228 | invert = bool(s.get("invert", dfl.get("invert", False))) 229 | divider_ratio = float(s.get("divider_ratio", 1.0)) 230 | r_pullup = float(s.get("r_pullup_ohm", dfl.get("r_pullup_ohm", 47000))) 231 | v_ref = float(s.get("v_ref", dfl.get("v_ref", 3.3))) 232 | 233 | # Mapping-Punkte: entweder explizit aus YAML oder linear generieren 234 | mp_items = s.get("mapping_points") 235 | if mp_items: 236 | mapping = normalize_mapping_points(mp_items, v_max=v_max, invert=invert) 237 | else: 238 | mapping = build_linear_mapping(v_max=v_max, invert=invert, steps=10) 239 | 240 | # Entitäten erstellen 241 | v_ent = ADSVoltageSensor(name, ain, divider_ratio) 242 | l_ent = ADSLevelSensor(name, v_ent, mapping) 243 | entities.extend([v_ent, l_ent]) 244 | 245 | if mode == "resistive": 246 | r_ent = ADSResistanceSensor(name, v_ent, r_pullup, v_ref) 247 | entities.append(r_ent) 248 | 249 | _LOGGER.debug( 250 | "Setup %s (CH%d→AIN%d): mode=%s v_max=%.2f invert=%s ratio=%.3f", 251 | name, ch_human, ain, mode, v_max, invert, divider_ratio 252 | ) 253 | 254 | async_add_entities(entities) 255 | -------------------------------------------------------------------------------- /info.md: -------------------------------------------------------------------------------- 1 | [![hacs_badge](https://img.shields.io/badge/HACS-Custom-orange.svg)](https://github.com/custom-components/hacs) 2 | 3 | ## homeassistant_PekawayVANPICORE 4 | 5 | Diese benutzerdefinierte Komponente integriert das VAN PI CORE Board mit Home Assistant und ermöglicht es dir, die Systeme deines Vans über die Home Assistant zu überwachen und zu steuern. 6 | 7 | ### Features 8 | 9 | - Integriert Inputs 1-8 10 | - Integriert Relais 1-8 11 | - Integriert MPU6050 (Beschleunigungs- und Lagersensor für die Van Ausrichtung) 12 | - Integriert ADS1115 (Wasserlevel) 13 | - Integriert bis zu 5 1-Wire Sensoren (als Temperatursensoren) 14 | - Integriert Uart 1 (RJ45) 15 | - Integriert Uart 2 (RJ11 LIN) 16 | - Integriert UART 4 (MPPT 75/15 Victron) 17 | - Integriert UART 5 (SmartShunt 500A/50mV Victron) 18 | 19 | 20 | # Installation 21 | 22 | ## 1. Vorbereitung des CORES für die Integration 23 | 24 | In der Datei mnt/boot/config.txt muss via SSH-Terminal folgendes hinzugefügt werden: 25 | 26 | **ℹ️ Die UART3 darf nicht angegeben werden auf diesem Pin liegt der 1-Wire Temp Sensor!** 27 | ``` 28 | dtparam=i2c_vc=on 29 | dtparam=i2c_arm=on 30 | dtoverlay=w1-gpio 31 | dtoverlay=uart1 32 | dtoverlay=uart2 33 | dtoverlay=uart4 34 | dtoverlay=uart5 35 | ``` 36 | Wenn du einen PI 5 hast musst du es so angeben: 37 | ``` 38 | dtparam=i2c_vc=on 39 | dtparam=i2c_arm=on 40 | dtoverlay=w1-gpio 41 | dtoverlay=uart5 42 | dtoverlay=uart0-pi5 43 | dtoverlay=uart4-pi5 44 | enable_uart=1 45 | ``` 46 | 47 | 48 | ### Um die config.txt anzupassen müssen wir uns auf den RPI Verbinden 49 | 50 | #### 1. Einstellungen -> Add-ons -> ADD-On Store -> SUCHE: Advanced SSH & Web Terminal 51 | 52 | Bildschirmfoto 2024-10-18 um 10 50 10 54 | 55 | 56 | ℹ️ Es gibt zwei Zugänge am Terminal: 57 | 58 | Port 22 welcher Zugriff zum Docker Container gibt 59 | Port 22222 welcher Zugriff direkt auf das Hauptsystem gibt. 60 | Da wir leider selbst über das Advanced Terminal nicht auf das Hautpsystem kommen müssen wir das via PC / Mac machen. 61 | 62 | - Ihr braucht einen **leeren USB-Stick** und ein **authorized_keyfile** 63 | (SSH-Key erstellen ([Windows](https://docs.digitalocean.com/products/droplets/how-to/add-ssh-keys/create-with-putty/), [Mac](https://docs.github.com/de/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent#platform-mac))) 64 | 65 | - Formatiere den USB-Stick mit FAT32 foratieren und benenne ihn `CONFIG` (case-sensetiv). 66 | - Auf den USB-Stick den Public Key als Textdatei mit dem namen **authorized_keys** (keine Erweiterung) kopieren 67 | 68 | - lege auf dem USB-Stick zusätzlich einen Ordner an mit dem Namen `modules` darin mit einer Datei namens: `rpi-i2c.conf` Inhalt der Datei: `i2c-dev` 69 | 70 | - Bildschirmfoto 2024-10-18 um 10 50 10 72 | 73 | - Den USB-Stick nun am RPI anschließen 74 | - Dann über das zuvor installierte SSH Web Termilal Plugin den Befehl 75 | ```ha os import``` eingeben. 76 | 77 | - Bildschirmfoto 2024-10-18 um 10 50 10 79 | 80 | - HomeAssistant rebooten (USB Stick muss beim ersten Start stecken bleiben, danach einfach wieder abziehen) 81 | 82 | - Beim Neustarten immer über `Entwicklerwerkzeuge`-> 'Konfiguration Prüfen`-> dann 'Neu Starten' -> 'Homeassistant Neustarten' 83 | (Das gibt die Sicherheit das alle Konfigurationen passen) 84 | 85 | - Bildschirmfoto 2024-10-18 um 10 50 10 87 | 88 | ℹ️ Wollt ihr das später wieder einmal deaktivieren Einfach einen leeren CONFIG benannten USB-Stick anstecken und das System neustarten, dann wird der Zugang wieder deaktiviert. 89 | 90 | Auf den RPI via Terminal verbinden, Mac über das Terminal: 91 | 92 | ``` 93 | ssh root@homeassistant.local -p 22222 94 | ``` 95 | 96 | Bildschirmfoto 2024-10-18 um 10 50 10 98 | 99 | Windows: Über Putty (Hier muss der Private Key (Endung .ppk) s über Connection -> ssh -> Auth hinterlegt werden). 100 | 101 | Die Oben angegebenen Zeilen via VI am Ende der config.txt hinzufügen und neustarten! 102 | 103 | ```` 104 | vi /mnt/boot/config.txt 105 | ```` 106 | 107 | ``` 108 | reboot 109 | ``` 110 | 111 | 112 | ## 2. Installation der Integration 113 | 114 | ## Installation über HACS Funtkioniert aktuell leider noch nicht! 115 | ~~ Nutze [HACS](https://hacs.xyz/) -> [Install and download anleitung ](https://hacs.xyz/docs/use/download/download/) -> Plugin schnell finden [hier](https://my.home-assistant.io/redirect/supervisor_addon/?addon=cb646a50_get&repository_url=https%3A%2F%2Fgithub.com%2Fhacs%2Faddons) klicken ~~ 116 | 117 | ~~ Füge https://github.com/maxlin1/homeassistant_PekawayVANPICORE zu Ihren [benutzerdefinierten Repositories](https://hacs.xyz/docs/faq/custom_repositories/) hinzu ~~ 118 | * .... 119 | 120 | ### So muss man aktuell noch Installieren: Manuelle Installation (empfohlen) 121 | * Klonen oder lade dieses Repository herunter 122 | * Verschiebe den inhalt des Ordners 'custom_components/' im Download in Ihren Home Assistant Konfigurationsordner das geht über `Studio Code Server` Siehe nächster Schritt via drag and drop 123 | 124 | Bildschirmfoto 2024-10-18 um 10 50 10 126 | 127 | * Starte Home Assistant neu 128 | 129 | ## 3. Einrichtung der Komponente 130 | 131 | Fürs bearbeiten der `configuration.yml` nütze ich den `Studio Code Server` es geht aber auch der `File editor` unter Einstellungen -> Ads-ons -> ADD-ON Store 132 | 133 | Bildschirmfoto 2024-10-18 um 10 50 10 135 | 136 | 137 | ### A. Die configuration.yaml bearbeiten 138 | * Füge die Konfiguration wie im Beispiel unten gezeigt in Ihre configuration.yaml ein 139 | ``` 140 | 141 | sensor: 142 | - platform: mpu6050 143 | 144 | - platform: victron_mppt 145 | port: "/dev/ttyAMA4" 146 | baudrate: 19200 147 | sleeptime: 10 148 | 149 | - platform: victron_smartshunt 150 | port: "/dev/ttyAMA5" 151 | baudrate: 19200 152 | sleeptime: 10 153 | 154 | - platform: ads_waterlevel 155 | 156 | # Paar-Defaults (optional, hier für Übersicht gesetzt) 157 | pair_config: 158 | "1-2": # CH1 & CH2 159 | mode: voltage # Spannung messen 160 | v_max: 3.3 161 | invert: false 162 | "3-4": # CH3 & CH4 163 | mode: resistive # Widerstand (Ω) berechnen + extra Entität 164 | v_max: 3.3 # nur für Level (Volt→Liter) relevant 165 | invert: false 166 | r_pullup_ohm: 47000 # interner Pull-Up ~47 kΩ 167 | v_ref: 3.3 168 | 169 | 170 | # Einzel-Sensoren (überschreiben ggf. die Paar-Defaults) 171 | sensors: 172 | # CH1 = Abwasser, Volt + eigenes Mapping 173 | - name: "Abwasser Tank" 174 | channel: 1 # Board-Kanal 1 175 | divider_ratio: 1.0 176 | mode: voltage 177 | mapping_points: 178 | - { v: 0.00, l: 0 } 179 | - { v: 0.49, l: 50 } 180 | - { v: 0.60, l: 60 } 181 | - { v: 0.81, l: 70 } 182 | - { v: 1.12, l: 80 } 183 | - { v: 1.25, l: 87 } 184 | - { v: 1.33, l: 90 } 185 | - { v: 1.44, l: 95 } 186 | - { v: 1.60, l: 100 } 187 | 188 | # CH2 = Volt, linear bis 2.50 V = 100 L 189 | - name: "Wasser Tank" 190 | channel: 2 # Board-Kanal 2 191 | divider_ratio: 1.0 192 | mode: voltage 193 | mapping_points: 194 | - { v: 0.00, l: 0 } 195 | - { v: 0.25, l: 10 } 196 | - { v: 0.50, l: 20 } 197 | - { v: 0.75, l: 30 } 198 | - { v: 1.00, l: 40 } 199 | - { v: 1.25, l: 50 } 200 | - { v: 1.50, l: 60 } 201 | - { v: 1.75, l: 70 } 202 | - { v: 2.00, l: 80 } 203 | - { v: 2.25, l: 90 } 204 | - { v: 2.50, l: 100 } 205 | 206 | # CH3 = Widerstand (Ohm) 207 | - name: "Tank 3" 208 | channel: 3 # Board-Kanal 3 209 | divider_ratio: 1.0 210 | mode: resistive # erzeugt zusätzlich "… Resistance" (Ω) 211 | # mapping_points: # optional: Volt→Liter-Kurve ergänzen, falls gewünscht 212 | 213 | # CH4 = Widerstand (Ohm) 214 | - name: "Tank 4" 215 | channel: 4 # Board-Kanal 4 216 | divider_ratio: 1.0 217 | mode: resistive 218 | # mapping_points: # optional 219 | 220 | switch: 221 | - platform: mpu6050 222 | 223 | - platform: mcp23017 224 | i2c_address: 0x20 225 | hw_sync: true 226 | pins: 227 | 8: Relais_1 228 | 9: Relais_2 229 | 10: Relais_3 230 | 11: Relais_4 231 | 12: Relais_5 232 | 13: Relais_6 233 | 14: Relais_7 234 | 15: Relais_8 235 | 236 | button: 237 | - platform: mpu6050 238 | 239 | binary_sensor: 240 | - platform: mcp23017 241 | i2c_address: 0x20 242 | invert_logic: true 243 | pins: 244 | 0: Button_1 245 | 1: Button_2 246 | 2: Button_3 247 | 3: Button_4 248 | 4: Button_5 249 | 5: Button_6 250 | 6: Button_7 251 | 7: Button_8 252 | 253 | ``` 254 | * Nun Starte Homeassistant neu 255 | 256 | ### Die 1-Wiere Sensoren werden über + Integration Hinzufügen geaddet 257 | 258 | Bildschirmfoto 2024-10-18 um 10 50 10 260 | 261 | Bildschirmfoto 2024-10-18 um 10 50 10 263 | 264 | Bildschirmfoto 2024-10-18 um 10 50 10 266 | 267 | Und Fertig :-) Alle Sensoren und Relais sind in HA. Viel Spaß beim benützen 268 | 269 | Bildschirmfoto 2024-10-18 um 10 50 10 271 | 272 | 273 | 274 | ### Need help? 275 | - Report bugs or issues on [GitHub](https://github.com/maxlin1/homeassistant_PekawayVANPICORE/issues) 276 | - Ask questions in the [Home Assistant Community](https://community.home-assistant.io) 277 | 278 | 279 | 280 | ## Danke an die Entwickler folgender Komponenten. Ich habe diese mit integriert 281 | 282 | - mcp23017: [Repository](https://github.com/jpcornil-git/HA-mcp23017) für die Relais und Eingänge 283 | - 1-Wire: [MCP23017 component](https://github.com/thecode/ha-onewire-sysbus) für die Temperatur Sensoren 284 | - [RPi GPIO expander](https://github.com/jpcornil-git/RPiHat_GPIO_Expander) 285 | -------------------------------------------------------------------------------- /custom_components/mcp23017/__init__.py: -------------------------------------------------------------------------------- 1 | """Support for I2C MCP23017 chip.""" 2 | 3 | import asyncio 4 | import functools 5 | import logging 6 | import threading 7 | import time 8 | 9 | import smbus2 10 | 11 | from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP 12 | from homeassistant.helpers import device_registry 13 | from homeassistant.components import persistent_notification 14 | 15 | from .const import ( 16 | CONF_FLOW_PIN_NUMBER, 17 | CONF_FLOW_PLATFORM, 18 | CONF_I2C_ADDRESS, 19 | DEFAULT_I2C_BUS, 20 | DEFAULT_SCAN_RATE, 21 | DOMAIN, 22 | ) 23 | 24 | # MCP23017 Register Map (IOCON.BANK = 1, MCP23008-compatible) 25 | IODIRA = 0x00 26 | IODIRB = 0x10 27 | IPOLA = 0x01 28 | IPOLB = 0x11 29 | GPINTENA = 0x02 30 | GPINTENB = 0x12 31 | DEFVALA = 0x03 32 | DEFVALB = 0x13 33 | INTCONA = 0x04 34 | INTCONB = 0x14 35 | IOCONA = 0x05 36 | IOCONB = 0x15 37 | GPPUA = 0x06 38 | GPPUB = 0x16 39 | INTFA = 0x07 40 | INTFB = 0x17 41 | INTCAPA = 0x08 42 | INTCAPB = 0x18 43 | GPIOA = 0x09 44 | GPIOB = 0x19 45 | OLATA = 0x0a 46 | OLATB = 0x1a 47 | 48 | # Register address used to toggle IOCON.BANK to 1 (only mapped when BANK is 0) 49 | IOCON_REMAP = 0x0b 50 | 51 | _LOGGER = logging.getLogger(__name__) 52 | 53 | PLATFORMS = ["binary_sensor", "switch"] 54 | 55 | MCP23017_DATA_LOCK = asyncio.Lock() 56 | 57 | class SetupEntryStatus: 58 | """Class registering the number of outstanding async_setup_entry calls.""" 59 | def __init__(self): 60 | """Initialize call counter.""" 61 | self.number = 0 62 | def __enter__(self): 63 | """Increment call counter (with statement).""" 64 | self.number +=1 65 | def __exit__(self, exc_type, exc_value, exc_tb): 66 | """Decrement call counter (with statement).""" 67 | self.number -=1 68 | def busy(self): 69 | """Return True when there is at least one outstanding call""" 70 | return self.number != 0 71 | 72 | setup_entry_status = SetupEntryStatus() 73 | 74 | 75 | async def async_setup(hass, config): 76 | """Set up the component.""" 77 | 78 | # hass.data[DOMAIN] stores one entry for each MCP23017 instance using i2c address as a key 79 | hass.data.setdefault(DOMAIN, {}) 80 | 81 | # Callback function to start polling when HA starts 82 | def start_polling(event): 83 | for component in hass.data[DOMAIN].values(): 84 | if not component.is_alive(): 85 | component.start_polling() 86 | 87 | # Callback function to stop polling when HA stops 88 | def stop_polling(event): 89 | for component in hass.data[DOMAIN].values(): 90 | if component.is_alive(): 91 | component.stop_polling() 92 | 93 | hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_polling) 94 | hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_polling) 95 | 96 | return True 97 | 98 | 99 | async def async_setup_entry(hass, config_entry): 100 | """Set up the MCP23017 from a config entry.""" 101 | 102 | # Register this setup instance 103 | with setup_entry_status: 104 | # Forward entry setup to configured platform 105 | await hass.config_entries.async_forward_entry_setups( 106 | config_entry, [config_entry.data[CONF_FLOW_PLATFORM]] 107 | ) 108 | 109 | return True 110 | 111 | 112 | async def async_unload_entry(hass, config_entry): 113 | """Unload entity from MCP23017 component and platform.""" 114 | # Unload related platform 115 | await hass.config_entries.async_forward_entry_unload( 116 | config_entry, config_entry.data[CONF_FLOW_PLATFORM] 117 | ) 118 | 119 | i2c_address = config_entry.data[CONF_I2C_ADDRESS] 120 | 121 | # DOMAIN data async mutex 122 | async with MCP23017_DATA_LOCK: 123 | if i2c_address in hass.data[DOMAIN]: 124 | component = hass.data[DOMAIN][i2c_address] 125 | 126 | # Unlink entity from component 127 | await hass.async_add_executor_job( 128 | functools.partial( 129 | component.unregister_entity, config_entry.data[CONF_FLOW_PIN_NUMBER] 130 | ) 131 | ) 132 | 133 | # Free component if not linked to any entities 134 | if component.has_no_entities: 135 | if component.is_alive(): 136 | await hass.async_add_executor_job(component.stop_polling) 137 | hass.data[DOMAIN].pop(i2c_address) 138 | 139 | _LOGGER.info( 140 | "%s@0x%02x component destroyed", 141 | type(component).__name__, 142 | i2c_address, 143 | ) 144 | else: 145 | _LOGGER.warning( 146 | "%s@0x%02x component not found, unable to unload entity (pin %d).", 147 | f"{DOMAIN}@0x{i2c_address:02x}", 148 | i2c_address, 149 | config_entry.data[CONF_FLOW_PIN_NUMBER], 150 | ) 151 | 152 | return True 153 | 154 | 155 | async def async_get_or_create(hass, config_entry, entity): 156 | """Get or create a MCP23017 component from entity i2c address.""" 157 | 158 | i2c_address = entity.address 159 | 160 | # DOMAIN data async mutex 161 | try: 162 | async with MCP23017_DATA_LOCK: 163 | if i2c_address in hass.data[DOMAIN]: 164 | component = hass.data[DOMAIN][i2c_address] 165 | else: 166 | # Try to create component when it doesn't exist 167 | component = await hass.async_add_executor_job( 168 | functools.partial(MCP23017, DEFAULT_I2C_BUS, i2c_address) 169 | ) 170 | hass.data[DOMAIN][i2c_address] = component 171 | 172 | # Start polling thread if hass is already running 173 | if hass.is_running: 174 | component.start_polling() 175 | 176 | # Register a device combining all related entities 177 | devices = device_registry.async_get(hass) 178 | devices.async_get_or_create( 179 | config_entry_id=config_entry.entry_id, 180 | identifiers={(DOMAIN, i2c_address)}, 181 | manufacturer="MicroChip", 182 | model=DOMAIN, 183 | name=f"{DOMAIN}@0x{i2c_address:02x}", 184 | ) 185 | 186 | # Link entity to component 187 | await hass.async_add_executor_job( 188 | functools.partial(component.register_entity, entity) 189 | ) 190 | 191 | except ValueError as error: 192 | component = None 193 | await hass.config_entries.async_remove(config_entry.entry_id) 194 | 195 | persistent_notification.create( 196 | hass, 197 | f"Error: Unable to access {DOMAIN}-0x{i2c_address:02x} ({error})", 198 | title=f"{DOMAIN} Configuration", 199 | notification_id=f"{DOMAIN} notification", 200 | ) 201 | 202 | return component 203 | 204 | 205 | def i2c_device_exist(address): 206 | try: 207 | smbus2.SMBus(DEFAULT_I2C_BUS).read_byte(address) 208 | except (FileNotFoundError, OSError) as error: 209 | return False 210 | return True 211 | 212 | 213 | class MCP23017(threading.Thread): 214 | """MCP23017 device driver.""" 215 | 216 | def __init__(self, bus, address): 217 | """Create a MCP23017 instance at {address} on I2C {bus}.""" 218 | self._address = address 219 | 220 | # Check device presence 221 | try: 222 | self._bus = smbus2.SMBus(bus) 223 | self._bus.read_byte(self._address) 224 | except (FileNotFoundError, OSError) as error: 225 | _LOGGER.error( 226 | "Unable to access %s (%s)", 227 | self.unique_id, 228 | error, 229 | ) 230 | raise ValueError(error) from error 231 | 232 | # Change register map (IOCON.BANK = 1) to support/make it compatible with MCP23008 233 | # - Note: when BANK is already set to 1, e.g. HA restart without power cycle, 234 | # IOCON_REMAP address is not mapped and write is ignored 235 | self[IOCON_REMAP] = self[IOCON_REMAP] | 0x80 236 | 237 | self._device_lock = threading.Lock() 238 | self._run = False 239 | self._cache = { 240 | "IODIR": (self[IODIRB] << 8) + self[IODIRA], 241 | "GPPU": (self[GPPUB] << 8) + self[GPPUA], 242 | "GPIO": (self[GPIOB] << 8) + self[GPIOA], 243 | "OLAT": (self[OLATB] << 8) + self[OLATA], 244 | } 245 | self._entities = [None for i in range(16)] 246 | self._update_bitmap = 0 247 | 248 | threading.Thread.__init__(self, name=self.unique_id) 249 | 250 | _LOGGER.info("%s device created", self.unique_id) 251 | 252 | def __enter__(self): 253 | """Lock access to device (with statement).""" 254 | self._device_lock.acquire() 255 | return self 256 | 257 | def __exit__(self, exception_type, exception_value, exception_traceback): 258 | """Unlock access to device (with statement).""" 259 | self._device_lock.release() 260 | return False 261 | 262 | def __setitem__(self, register, value): 263 | """Set MCP23017 {register} to {value}.""" 264 | self._bus.write_byte_data(self._address, register, value) 265 | 266 | def __getitem__(self, register): 267 | """Get value of MCP23017 {register}.""" 268 | data = self._bus.read_byte_data(self._address, register) 269 | return data 270 | 271 | def _get_register_value(self, register, bit): 272 | """Get MCP23017 {bit} of {register}.""" 273 | if bit < 8: 274 | value = self[globals()[register + "A"]] & 0xFF 275 | self._cache[register] = self._cache[register] & 0xFF00 | value 276 | else: 277 | value = self[globals()[register + "B"]] & 0xFF 278 | self._cache[register] = self._cache[register] & 0x00FF | (value << 8) 279 | 280 | return bool(self._cache[register] & (1 << bit)) 281 | 282 | def _set_register_value(self, register, bit, value): 283 | """Set MCP23017 {bit} of {register} to {value}.""" 284 | # Update cache 285 | cache_old = self._cache[register] 286 | if value: 287 | self._cache[register] |= (1 << bit) & 0xFFFF 288 | else: 289 | self._cache[register] &= ~(1 << bit) & 0xFFFF 290 | # Update device register only if required (minimize # of I2C transactions) 291 | if cache_old != self._cache[register]: 292 | if bit < 8: 293 | self[globals()[register + "A"]] = self._cache[register] & 0xFF 294 | else: 295 | self[globals()[register + "B"]] = (self._cache[register] >> 8) & 0xFF 296 | 297 | @property 298 | def address(self): 299 | """Return device address.""" 300 | return self._address 301 | 302 | @property 303 | def unique_id(self): 304 | """Return component unique id.""" 305 | return f"{DOMAIN}-0x{self.address:02x}" 306 | 307 | @property 308 | def has_no_entities(self): 309 | """Check if there are no more entities attached.""" 310 | return not any(self._entities) 311 | 312 | # -- Called from HA thread pool 313 | 314 | def get_pin_value(self, pin): 315 | """Get MCP23017 GPIO[{pin}] value.""" 316 | with self: 317 | return self._get_register_value("GPIO", pin) 318 | 319 | def set_pin_value(self, pin, value): 320 | """Set MCP23017 GPIO[{pin}] to {value}.""" 321 | with self: 322 | self._set_register_value("OLAT", pin, value) 323 | 324 | def set_input(self, pin, is_input): 325 | """Set MCP23017 GPIO[{pin}] as input.""" 326 | with self: 327 | self._set_register_value("IODIR", pin, is_input) 328 | 329 | def set_pullup(self, pin, is_pullup): 330 | """Set MCP23017 GPIO[{pin}] as pullup.""" 331 | with self: 332 | self._set_register_value("GPPU", pin, is_pullup) 333 | 334 | def register_entity(self, entity): 335 | """Register entity to this device instance.""" 336 | with self: 337 | self._entities[entity.pin] = entity 338 | 339 | # Trigger a callback to update initial state 340 | self._update_bitmap |= (1 << entity.pin) & 0xFFFF 341 | 342 | _LOGGER.info( 343 | "%s(pin %d:'%s') attached to %s", 344 | type(entity).__name__, 345 | entity.pin, 346 | entity.name, 347 | self.unique_id, 348 | ) 349 | 350 | return True 351 | 352 | def unregister_entity(self, pin_number): 353 | """Unregister entity from the device.""" 354 | with self: 355 | entity = self._entities[pin_number] 356 | entity.unsubscribe_update_listener() 357 | self._entities[pin_number] = None 358 | 359 | _LOGGER.info( 360 | "%s(pin %d:'%s') removed from %s", 361 | type(entity).__name__, 362 | entity.pin, 363 | entity.name, 364 | self.unique_id, 365 | ) 366 | 367 | # -- Threading components 368 | 369 | def start_polling(self): 370 | """Start polling thread.""" 371 | self._run = True 372 | self.start() 373 | 374 | def stop_polling(self): 375 | """Stop polling thread.""" 376 | self._run = False 377 | self.join() 378 | 379 | def run(self): 380 | """Poll all ports once and call corresponding callback if a change is detected.""" 381 | 382 | _LOGGER.info("%s start polling thread", self.unique_id) 383 | 384 | while self._run: 385 | with self: 386 | # Read pin values for bank A and B from device only if there are associated callbacks (minimize # of I2C transactions) 387 | input_state = self._cache["GPIO"] 388 | if any( 389 | hasattr(entity, "push_update") for entity in self._entities[0:8] 390 | ): 391 | input_state = input_state & 0xFF00 | self[GPIOA] 392 | if any( 393 | hasattr(entity, "push_update") for entity in self._entities[8:16] 394 | ): 395 | input_state = input_state & 0x00FF | (self[GPIOB] << 8) 396 | 397 | # Check pin values that changed and update input cache 398 | self._update_bitmap = self._update_bitmap | ( 399 | input_state ^ self._cache["GPIO"] 400 | ) 401 | self._cache["GPIO"] = input_state 402 | # Call callback functions only for pin that changed 403 | for pin in range(16): 404 | if (self._update_bitmap & 0x1) and hasattr( 405 | self._entities[pin], "push_update" 406 | ): 407 | self._entities[pin].push_update(bool(input_state & 0x1)) 408 | input_state >>= 1 409 | self._update_bitmap >>= 1 410 | 411 | time.sleep(DEFAULT_SCAN_RATE) 412 | 413 | _LOGGER.info("%s stop polling thread", self.unique_id) 414 | --------------------------------------------------------------------------------