├── 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 |
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 |
23 |
24 |
25 | ### Erweiterte Modus einschalten
26 |
27 | Unten Links auf den 'Usernamen' klicken um dann den -> Erweiterten Modus einschalten.
28 |
29 |
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 | [](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 |
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 | -
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 | -
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 | -
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 |
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 |
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 |
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 |
260 |
261 |
263 |
264 |
266 |
267 | Und Fertig :-) Alle Sensoren und Relais sind in HA. Viel Spaß beim benützen
268 |
269 |
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 |
--------------------------------------------------------------------------------