├── tests ├── __init__.py └── test_config_flow.py ├── images ├── energy_dashboard.png └── energy_distribution.png ├── hacs.json ├── .github └── workflows │ ├── hassfest.yaml │ └── hacs.yaml ├── custom_components └── senec │ ├── manifest.json │ ├── translations │ └── en.json │ ├── strings.json │ ├── sensor.py │ ├── config_flow.py │ ├── __init__.py │ └── const.py ├── .pre-commit-config.yaml ├── README.md ├── .gitignore └── LICENSE /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests for the senec integration.""" 2 | -------------------------------------------------------------------------------- /images/energy_dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mchwalisz/home-assistant-senec/HEAD/images/energy_dashboard.png -------------------------------------------------------------------------------- /images/energy_distribution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mchwalisz/home-assistant-senec/HEAD/images/energy_distribution.png -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Senec solar system sensor", 3 | "country": [ 4 | "DE" 5 | ], 6 | "homeassistant": "2021.12.8", 7 | "hacs": "1.18.0", 8 | "render_readme": true 9 | } 10 | -------------------------------------------------------------------------------- /.github/workflows/hassfest.yaml: -------------------------------------------------------------------------------- 1 | name: Validate with hassfest 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 0 * * *' 8 | 9 | jobs: 10 | validate: 11 | runs-on: "ubuntu-latest" 12 | steps: 13 | - uses: "actions/checkout@v2" 14 | - uses: "home-assistant/actions/hassfest@master" 15 | -------------------------------------------------------------------------------- /.github/workflows/hacs.yaml: -------------------------------------------------------------------------------- 1 | name: HACS Action 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: "0 0 * * *" 8 | 9 | jobs: 10 | hacs: 11 | name: HACS Action 12 | runs-on: "ubuntu-latest" 13 | steps: 14 | - uses: "actions/checkout@v2" 15 | - name: HACS Action 16 | uses: "hacs/action@main" 17 | with: 18 | category: "integration" 19 | -------------------------------------------------------------------------------- /custom_components/senec/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "senec", 3 | "name": "senec", 4 | "config_flow": true, 5 | "documentation": "https://github.com/mchwalisz/home-assistant-senec/blob/master/README.md", 6 | "issue_tracker": "https://github.com/mchwalisz/home-assistant-senec/issues", 7 | "requirements": [ 8 | "pysenec==0.3.0" 9 | ], 10 | "dependencies": [], 11 | "codeowners": [ 12 | "@mchwalisz" 13 | ], 14 | "iot_class": "local_polling", 15 | "version": "2.0.0" 16 | } 17 | -------------------------------------------------------------------------------- /custom_components/senec/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "Integration is already configured" 5 | }, 6 | "error": { 7 | "cannot_connect": "Failed to connect", 8 | "unknown": "Unknown error" 9 | }, 10 | "step": { 11 | "user": { 12 | "data": { 13 | "host": "IP or hostname of the Senec Device" 14 | } 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /custom_components/senec/strings.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "data": { 6 | "host": "[%key:common::config_flow::data::host%]", 7 | "username": "[%key:common::config_flow::data::username%]", 8 | "password": "[%key:common::config_flow::data::password%]" 9 | } 10 | } 11 | }, 12 | "error": { 13 | "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", 14 | "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", 15 | "unknown": "[%key:common::config_flow::error::unknown%]" 16 | }, 17 | "abort": { 18 | "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.2.0 4 | hooks: 5 | - id: check-yaml 6 | - id: end-of-file-fixer 7 | - id: trailing-whitespace 8 | - repo: https://github.com/psf/black 9 | rev: 22.3.0 10 | hooks: 11 | - id: black 12 | args: 13 | - --line-length=100 14 | # these folders wont be formatted by black 15 | - --exclude="""\.git | 16 | \.__pycache__| 17 | \.hg| 18 | \.mypy_cache| 19 | \.tox| 20 | \.venv| 21 | _build| 22 | buck-out| 23 | build| 24 | dist""" 25 | - repo: https://github.com/PyCQA/isort 26 | rev: '5.10.1' 27 | hooks: 28 | - id: isort 29 | name: isort 30 | entry: isort 31 | require_serial: true 32 | language: python 33 | types: [python] 34 | args: 35 | - --multi-line=3 36 | - --line-length=100 37 | - --profile=black 38 | -------------------------------------------------------------------------------- /custom_components/senec/sensor.py: -------------------------------------------------------------------------------- 1 | """Platform for Senec sensors.""" 2 | import logging 3 | 4 | import homeassistant.helpers.config_validation as cv 5 | from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN 6 | from homeassistant.components.sensor import SensorEntity, SensorEntityDescription 7 | from homeassistant.config_entries import ConfigEntry 8 | from homeassistant.helpers.typing import HomeAssistantType 9 | 10 | from . import SenecDataUpdateCoordinator, SenecEntity 11 | from .const import DOMAIN, SENSOR_TYPES 12 | 13 | _LOGGER = logging.getLogger(__name__) 14 | 15 | 16 | async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities): 17 | """Initialize sensor platform from config entry.""" 18 | coordinator = hass.data[DOMAIN][config_entry.entry_id] 19 | entities = [] 20 | for description in SENSOR_TYPES: 21 | entity = SenecSensor(coordinator, description) 22 | entities.append(entity) 23 | 24 | async_add_entities(entities) 25 | 26 | 27 | class SenecSensor(SenecEntity, SensorEntity): 28 | """Sensor for the single values (e.g. pv power, ac power).""" 29 | 30 | def __init__( 31 | self, 32 | coordinator: SenecDataUpdateCoordinator, 33 | description: SensorEntityDescription, 34 | ): 35 | """Initialize a singular value sensor.""" 36 | super().__init__(coordinator=coordinator, description=description) 37 | 38 | title = self.coordinator._entry.title 39 | key = self.entity_description.key 40 | name = self.entity_description.name 41 | self.entity_id = f"sensor.{title}_{key}" 42 | self._attr_name = f"{title} {name}" 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Home Assistant sensor for Senec solar systems 2 | 3 | ## Installation 4 | 5 | ### Hacs 6 | 7 | [![hacs_badge](https://img.shields.io/badge/HACS-Default-41BDF5.svg)](https://github.com/hacs/integration) 8 | 9 | - Install [Home Assistant Community Store (HACS)](https://hacs.xyz/) 10 | - Add custom repository https://github.com/mchwalisz/home-assistant-senec to HACS 11 | - Add integration repository (search for "Senec" in "Explore & Download Repositories") 12 | - Select latest version or `master` 13 | - Restart Home Assistant to install all dependencies 14 | 15 | ### Manual 16 | 17 | - Copy all files from `custom_components/senec/` to `custom_components/senec/` inside your config Home Assistant directory. 18 | - Restart Home Assistant to install all dependencies 19 | 20 | ### Adding or enabling integration 21 | #### My Home Assistant (2021.3+) 22 | [![Open your Home Assistant instance and start setting up a new integration.](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=senec) 23 | 24 | #### Manual 25 | Add custom integration using the web interface and follow instruction on screen. 26 | 27 | - Go to `Configuration -> Integrations` and add "Senec" integration 28 | - Provide name for the device and it's address (hostname or IP) 29 | - Provide area where the battery is located 30 | 31 | ## Home Assistant Energy Dashboard 32 | 33 | This integration supports Home Assistant's [Energy Management](https://www.home-assistant.io/docs/energy/) 34 | 35 | Example setup: 36 | 37 | ![Energy Dashboard Setup](images/energy_dashboard.png) 38 | 39 | Resulting energy distribution card: 40 | 41 | ![Energy Distribution](images/energy_distribution.png) 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/python 3 | # Edit at https://www.gitignore.io/?templates=python 4 | 5 | ### Python ### 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | pip-wheel-metadata/ 29 | share/python-wheels/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | MANIFEST 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .nox/ 49 | .coverage 50 | .coverage.* 51 | .cache 52 | nosetests.xml 53 | coverage.xml 54 | *.cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # pyenv 72 | .python-version 73 | 74 | # pipenv 75 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 76 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 77 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 78 | # install all needed dependencies. 79 | #Pipfile.lock 80 | 81 | # celery beat schedule file 82 | celerybeat-schedule 83 | 84 | # SageMath parsed files 85 | *.sage.py 86 | 87 | # Spyder project settings 88 | .spyderproject 89 | .spyproject 90 | 91 | # Rope project settings 92 | .ropeproject 93 | 94 | # Mr Developer 95 | .mr.developer.cfg 96 | .project 97 | .pydevproject 98 | 99 | # mkdocs documentation 100 | /site 101 | 102 | # mypy 103 | .mypy_cache/ 104 | .dmypy.json 105 | dmypy.json 106 | 107 | # Pyre type checker 108 | .pyre/ 109 | 110 | # End of https://www.gitignore.io/api/python 111 | -------------------------------------------------------------------------------- /tests/test_config_flow.py: -------------------------------------------------------------------------------- 1 | """Test the senec config flow.""" 2 | from homeassistant import config_entries, setup 3 | from homeassistant.components.senec.config_flow import CannotConnect, InvalidAuth 4 | from homeassistant.components.senec.const import DOMAIN 5 | 6 | from tests.async_mock import patch 7 | 8 | 9 | async def test_form(hass): 10 | """Test we get the form.""" 11 | await setup.async_setup_component(hass, "persistent_notification", {}) 12 | result = await hass.config_entries.flow.async_init( 13 | DOMAIN, context={"source": config_entries.SOURCE_USER} 14 | ) 15 | assert result["type"] == "form" 16 | assert result["errors"] == {} 17 | 18 | with patch( 19 | "homeassistant.components.senec.config_flow.PlaceholderHub.authenticate", 20 | return_value=True, 21 | ), patch("homeassistant.components.senec.async_setup", return_value=True) as mock_setup, patch( 22 | "homeassistant.components.senec.async_setup_entry", 23 | return_value=True, 24 | ) as mock_setup_entry: 25 | result2 = await hass.config_entries.flow.async_configure( 26 | result["flow_id"], 27 | { 28 | "host": "1.1.1.1", 29 | "username": "test-username", 30 | "password": "test-password", 31 | }, 32 | ) 33 | 34 | assert result2["type"] == "create_entry" 35 | assert result2["title"] == "Name of the device" 36 | assert result2["data"] == { 37 | "host": "1.1.1.1", 38 | "username": "test-username", 39 | "password": "test-password", 40 | } 41 | await hass.async_block_till_done() 42 | assert len(mock_setup.mock_calls) == 1 43 | assert len(mock_setup_entry.mock_calls) == 1 44 | 45 | 46 | async def test_form_invalid_auth(hass): 47 | """Test we handle invalid auth.""" 48 | result = await hass.config_entries.flow.async_init( 49 | DOMAIN, context={"source": config_entries.SOURCE_USER} 50 | ) 51 | 52 | with patch( 53 | "homeassistant.components.senec.config_flow.PlaceholderHub.authenticate", 54 | side_effect=InvalidAuth, 55 | ): 56 | result2 = await hass.config_entries.flow.async_configure( 57 | result["flow_id"], 58 | { 59 | "host": "1.1.1.1", 60 | "username": "test-username", 61 | "password": "test-password", 62 | }, 63 | ) 64 | 65 | assert result2["type"] == "form" 66 | assert result2["errors"] == {"base": "invalid_auth"} 67 | 68 | 69 | async def test_form_cannot_connect(hass): 70 | """Test we handle cannot connect error.""" 71 | result = await hass.config_entries.flow.async_init( 72 | DOMAIN, context={"source": config_entries.SOURCE_USER} 73 | ) 74 | 75 | with patch( 76 | "homeassistant.components.senec.config_flow.PlaceholderHub.authenticate", 77 | side_effect=CannotConnect, 78 | ): 79 | result2 = await hass.config_entries.flow.async_configure( 80 | result["flow_id"], 81 | { 82 | "host": "1.1.1.1", 83 | "username": "test-username", 84 | "password": "test-password", 85 | }, 86 | ) 87 | 88 | assert result2["type"] == "form" 89 | assert result2["errors"] == {"base": "cannot_connect"} 90 | -------------------------------------------------------------------------------- /custom_components/senec/config_flow.py: -------------------------------------------------------------------------------- 1 | """Config flow for senec integration.""" 2 | import logging 3 | from urllib.parse import ParseResult, urlparse 4 | 5 | import voluptuous as vol 6 | from homeassistant import config_entries 7 | from homeassistant.const import CONF_HOST, CONF_NAME 8 | from homeassistant.core import HomeAssistant, callback 9 | from homeassistant.util import slugify 10 | from pysenec import Senec 11 | from requests.exceptions import HTTPError, Timeout 12 | 13 | from .const import DOMAIN # pylint:disable=unused-import 14 | from .const import DEFAULT_HOST, DEFAULT_NAME 15 | 16 | _LOGGER = logging.getLogger(__name__) 17 | 18 | 19 | @callback 20 | def senec_entries(hass: HomeAssistant): 21 | """Return the hosts already configured.""" 22 | return {entry.data[CONF_HOST] for entry in hass.config_entries.async_entries(DOMAIN)} 23 | 24 | 25 | class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 26 | """Handle a config flow for senec.""" 27 | 28 | VERSION = 1 29 | CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL 30 | 31 | def _host_in_configuration_exists(self, host) -> bool: 32 | """Return True if host exists in configuration.""" 33 | if host in senec_entries(self.hass): 34 | return True 35 | return False 36 | 37 | async def _test_connection(self, host): 38 | """Check if we can connect to the Senec device.""" 39 | websession = self.hass.helpers.aiohttp_client.async_get_clientsession() 40 | try: 41 | senec_client = Senec(host, websession) 42 | await senec_client.update() 43 | return True 44 | except (OSError, HTTPError, Timeout): 45 | self._errors[CONF_HOST] = "cannot_connect" 46 | _LOGGER.error( 47 | "Could not connect to Senec device at %s, check host ip address", 48 | host, 49 | ) 50 | return False 51 | 52 | async def async_step_user(self, user_input=None): 53 | """Step when user initializes a integration.""" 54 | self._errors = {} 55 | if user_input is not None: 56 | # set some defaults in case we need to return to the form 57 | name = slugify(user_input.get(CONF_NAME, DEFAULT_NAME)) 58 | host_entry = user_input.get(CONF_HOST, DEFAULT_HOST) 59 | 60 | if self._host_in_configuration_exists(host_entry): 61 | self._errors[CONF_HOST] = "already_configured" 62 | else: 63 | if await self._test_connection(host_entry): 64 | return self.async_create_entry(title=name, data={CONF_HOST: host_entry}) 65 | else: 66 | user_input = {} 67 | user_input[CONF_NAME] = DEFAULT_NAME 68 | user_input[CONF_HOST] = DEFAULT_HOST 69 | 70 | return self.async_show_form( 71 | step_id="user", 72 | data_schema=vol.Schema( 73 | { 74 | vol.Required(CONF_NAME, default=user_input.get(CONF_NAME, DEFAULT_NAME)): str, 75 | vol.Required(CONF_HOST, default=user_input.get(CONF_HOST, DEFAULT_HOST)): str, 76 | } 77 | ), 78 | errors=self._errors, 79 | ) 80 | 81 | async def async_step_import(self, user_input=None): 82 | """Import a config entry.""" 83 | host_entry = user_input.get(CONF_HOST, DEFAULT_HOST) 84 | 85 | if self._host_in_configuration_exists(host_entry): 86 | return self.async_abort(reason="already_configured") 87 | return await self.async_step_user(user_input) 88 | -------------------------------------------------------------------------------- /custom_components/senec/__init__.py: -------------------------------------------------------------------------------- 1 | """The senec integration.""" 2 | import asyncio 3 | import logging 4 | from datetime import timedelta 5 | 6 | import async_timeout 7 | import voluptuous as vol 8 | from homeassistant.config_entries import ConfigEntry 9 | from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SCAN_INTERVAL 10 | from homeassistant.core import HomeAssistant 11 | from homeassistant.exceptions import ConfigEntryNotReady 12 | from homeassistant.helpers.aiohttp_client import async_get_clientsession 13 | from homeassistant.helpers.entity import Entity, EntityDescription 14 | from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed 15 | from pysenec import Senec 16 | 17 | from .const import DEFAULT_HOST, DEFAULT_NAME, DOMAIN, SCAN_INTERVAL 18 | 19 | _LOGGER = logging.getLogger(__name__) 20 | 21 | CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) 22 | 23 | PLATFORMS = ["sensor"] 24 | 25 | 26 | async def async_setup(hass: HomeAssistant, config: dict): 27 | """Set up the senec component.""" 28 | return True 29 | 30 | 31 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): 32 | """Set up senec from a config entry.""" 33 | session = async_get_clientsession(hass) 34 | 35 | coordinator = SenecDataUpdateCoordinator(hass, session, entry) 36 | 37 | await coordinator.async_refresh() 38 | 39 | if not coordinator.last_update_success: 40 | raise ConfigEntryNotReady 41 | 42 | hass.data.setdefault(DOMAIN, {}) 43 | hass.data[DOMAIN][entry.entry_id] = coordinator 44 | 45 | for platform in PLATFORMS: 46 | hass.async_create_task(hass.config_entries.async_forward_entry_setup(entry, platform)) 47 | 48 | return True 49 | 50 | 51 | class SenecDataUpdateCoordinator(DataUpdateCoordinator): 52 | """Define an object to hold Senec data.""" 53 | 54 | def __init__(self, hass, session, entry): 55 | """Initialize.""" 56 | self._host = entry.data[CONF_HOST] 57 | self.senec = Senec(self._host, websession=session) 58 | self.name = entry.title 59 | self._entry = entry 60 | 61 | super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) 62 | 63 | async def _async_update_data(self): 64 | """Update data via library.""" 65 | with async_timeout.timeout(20): 66 | await self.senec.update() 67 | return self.senec 68 | 69 | 70 | async def async_unload_entry(hass, entry): 71 | """Unload Senec config entry.""" 72 | unload_ok = all( 73 | await asyncio.gather( 74 | *[ 75 | hass.config_entries.async_forward_entry_unload(entry, component) 76 | for component in PLATFORMS 77 | ] 78 | ) 79 | ) 80 | if unload_ok: 81 | hass.data[DOMAIN].pop(entry.entry_id) 82 | return unload_ok 83 | 84 | 85 | class SenecEntity(Entity): 86 | """Defines a base Senec entity.""" 87 | 88 | _attr_should_poll = False 89 | 90 | def __init__( 91 | self, coordinator: SenecDataUpdateCoordinator, description: EntityDescription 92 | ) -> None: 93 | """Initialize the Atag entity.""" 94 | self.coordinator = coordinator 95 | self._name = coordinator._entry.title 96 | self._state = None 97 | 98 | self.entity_description = description 99 | 100 | @property 101 | def device_info(self) -> dict: 102 | """Return info for device registry.""" 103 | device = self._name 104 | return { 105 | "identifiers": {(DOMAIN, device)}, 106 | "name": "Senec Home Battery", 107 | "model": "Senec", 108 | "sw_version": None, 109 | "manufacturer": "Senec", 110 | } 111 | 112 | @property 113 | def state(self): 114 | """Return the current state.""" 115 | sensor = self.entity_description.key 116 | value = getattr(self.coordinator.senec, sensor) 117 | try: 118 | rounded_value = round(float(value), 2) 119 | return rounded_value 120 | except ValueError: 121 | return value 122 | 123 | @property 124 | def available(self): 125 | """Return True if entity is available.""" 126 | return self.coordinator.last_update_success 127 | 128 | @property 129 | def unique_id(self): 130 | """Return a unique ID to use for this entity.""" 131 | sensor = self.entity_description.key 132 | return f"{self._name}_{sensor}" 133 | 134 | async def async_added_to_hass(self): 135 | """Connect to dispatcher listening for entity data notifications.""" 136 | self.async_on_remove(self.coordinator.async_add_listener(self.async_write_ha_state)) 137 | 138 | async def async_update(self): 139 | """Update entity.""" 140 | await self.coordinator.async_request_refresh() 141 | -------------------------------------------------------------------------------- /custom_components/senec/const.py: -------------------------------------------------------------------------------- 1 | """Constants for the Senec integration.""" 2 | from collections import namedtuple 3 | from datetime import timedelta 4 | from typing import Final 5 | 6 | from homeassistant.components.sensor import ( 7 | SensorDeviceClass, 8 | SensorEntityDescription, 9 | SensorStateClass, 10 | ) 11 | from homeassistant.const import ENERGY_KILO_WATT_HOUR, PERCENTAGE, POWER_WATT, TEMP_CELSIUS 12 | 13 | DOMAIN = "senec" 14 | 15 | 16 | """Default config for Senec.""" 17 | DEFAULT_HOST = "Senec" 18 | DEFAULT_NAME = "senec" 19 | 20 | """Fixed constants.""" 21 | SCAN_INTERVAL = timedelta(seconds=60) 22 | 23 | """Supported sensor types.""" 24 | 25 | SENSOR_TYPES = [ 26 | SensorEntityDescription( 27 | key="system_state", 28 | name="System State", 29 | icon="mdi:solar-power", 30 | ), 31 | SensorEntityDescription( 32 | key="battery_temp", 33 | name="Battery Temperature", 34 | native_unit_of_measurement=TEMP_CELSIUS, 35 | icon="mdi:thermometer", 36 | ), 37 | SensorEntityDescription( 38 | key="case_temp", 39 | name="Case Temperature", 40 | native_unit_of_measurement=TEMP_CELSIUS, 41 | icon="mdi:thermometer", 42 | ), 43 | SensorEntityDescription( 44 | key="mcu_temp", 45 | name="Controller Temperature", 46 | native_unit_of_measurement=TEMP_CELSIUS, 47 | icon="mdi:thermometer", 48 | ), 49 | SensorEntityDescription( 50 | key="solar_generated_power", 51 | name="Solar Generated Power", 52 | native_unit_of_measurement=POWER_WATT, 53 | icon="mdi:solar-power", 54 | device_class=SensorDeviceClass.POWER, 55 | state_class=SensorStateClass.MEASUREMENT, 56 | ), 57 | SensorEntityDescription( 58 | key="house_power", 59 | name="House Power", 60 | native_unit_of_measurement=POWER_WATT, 61 | icon="mdi:home-import-outline", 62 | device_class=SensorDeviceClass.POWER, 63 | state_class=SensorStateClass.MEASUREMENT, 64 | ), 65 | SensorEntityDescription( 66 | key="battery_state_power", 67 | name="Battery State Power", 68 | native_unit_of_measurement=POWER_WATT, 69 | icon="mdi:home-battery", 70 | device_class=SensorDeviceClass.POWER, 71 | state_class=SensorStateClass.MEASUREMENT, 72 | ), 73 | SensorEntityDescription( 74 | key="battery_charge_power", 75 | name="Battery Charge Power", 76 | native_unit_of_measurement=POWER_WATT, 77 | icon="mdi:home-battery", 78 | device_class=SensorDeviceClass.POWER, 79 | state_class=SensorStateClass.MEASUREMENT, 80 | ), 81 | SensorEntityDescription( 82 | key="battery_discharge_power", 83 | name="Battery Discharge Power", 84 | native_unit_of_measurement=POWER_WATT, 85 | icon="mdi:home-battery-outline", 86 | device_class=SensorDeviceClass.POWER, 87 | state_class=SensorStateClass.MEASUREMENT, 88 | ), 89 | SensorEntityDescription( 90 | key="battery_charge_percent", 91 | name="Battery Charge Percent", 92 | native_unit_of_measurement=PERCENTAGE, 93 | icon="mdi:home-battery", 94 | # device_class=SensorDeviceClass.BATTERY, 95 | state_class=SensorStateClass.MEASUREMENT, 96 | ), 97 | SensorEntityDescription( 98 | key="grid_state_power", 99 | name="Grid State Power", 100 | native_unit_of_measurement=POWER_WATT, 101 | icon="mdi:transmission-tower", 102 | device_class=SensorDeviceClass.POWER, 103 | state_class=SensorStateClass.MEASUREMENT, 104 | ), 105 | SensorEntityDescription( 106 | key="grid_imported_power", 107 | name="Grid Imported Power", 108 | native_unit_of_measurement=POWER_WATT, 109 | icon="mdi:transmission-tower-import", 110 | device_class=SensorDeviceClass.POWER, 111 | state_class=SensorStateClass.MEASUREMENT, 112 | ), 113 | SensorEntityDescription( 114 | key="grid_exported_power", 115 | name="Grid Exported Power", 116 | native_unit_of_measurement=POWER_WATT, 117 | icon="mdi:transmission-tower-export", 118 | device_class=SensorDeviceClass.POWER, 119 | state_class=SensorStateClass.MEASUREMENT, 120 | ), 121 | SensorEntityDescription( 122 | key="house_total_consumption", 123 | name="House consumed", 124 | native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, 125 | icon="mdi:home-import-outline", 126 | device_class=SensorDeviceClass.ENERGY, 127 | state_class=SensorStateClass.TOTAL_INCREASING, 128 | ), 129 | SensorEntityDescription( 130 | key="solar_total_generated", 131 | name="Solar generated", 132 | native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, 133 | icon="mdi:solar-power", 134 | device_class=SensorDeviceClass.ENERGY, 135 | state_class=SensorStateClass.TOTAL_INCREASING, 136 | ), 137 | SensorEntityDescription( 138 | key="battery_total_charged", 139 | name="Battery charged", 140 | native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, 141 | icon="mdi:home-battery", 142 | device_class=SensorDeviceClass.ENERGY, 143 | state_class=SensorStateClass.TOTAL_INCREASING, 144 | ), 145 | SensorEntityDescription( 146 | key="battery_total_discharged", 147 | name="Battery discharged", 148 | native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, 149 | icon="mdi:home-battery-outline", 150 | device_class=SensorDeviceClass.ENERGY, 151 | state_class=SensorStateClass.TOTAL_INCREASING, 152 | ), 153 | SensorEntityDescription( 154 | key="grid_total_import", 155 | name="Grid Imported", 156 | native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, 157 | icon="mdi:transmission-tower-import", 158 | device_class=SensorDeviceClass.ENERGY, 159 | state_class=SensorStateClass.TOTAL_INCREASING, 160 | ), 161 | SensorEntityDescription( 162 | key="grid_total_export", 163 | name="Grid Exported", 164 | native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, 165 | icon="mdi:transmission-tower-export", 166 | device_class=SensorDeviceClass.ENERGY, 167 | state_class=SensorStateClass.TOTAL_INCREASING, 168 | ), 169 | ] 170 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------