Skip to content

Commit

Permalink
feat: push self production sensors to openevse (#239)
Browse files Browse the repository at this point in the history
* feat: push self production sensors to openevse

* update test

* update test again

* fix config_flow

* formatting

* default invert to false
fix translations

* actually pull sensor data

* round sensor values

* code optimization

* fix handle_state_change

* get sensor states

* type the sensor states

* await the call

* add debug message

* spelling error

* account for unavailable sensors

* account for none value or unavailable value
  • Loading branch information
firstof9 authored Jul 29, 2023
1 parent fedf239 commit 29d0d63
Show file tree
Hide file tree
Showing 8 changed files with 137 additions and 13 deletions.
88 changes: 86 additions & 2 deletions custom_components/openevse/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,31 @@
from __future__ import annotations

import asyncio
import functools
import logging
from datetime import timedelta

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import Config, HomeAssistant, callback
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
CONF_USERNAME,
EVENT_HOMEASSISTANT_STARTED,
)
from homeassistant.core import Config, CoreState, HomeAssistant, callback, Event, State
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.event import async_track_state_change
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from openevsehttp.__main__ import OpenEVSE
from openevsehttp.exceptions import MissingSerial

from .const import (
BINARY_SENSORS,
CONF_NAME,
CONF_GRID,
CONF_INVERT,
CONF_SOLAR,
COORDINATOR,
DOMAIN,
FW_COORDINATOR,
Expand All @@ -25,13 +35,64 @@
PLATFORMS,
SELECT_TYPES,
SENSOR_TYPES,
UNSUB_LISTENERS,
VERSION,
)
from .services import OpenEVSEServices

_LOGGER = logging.getLogger(__name__)


async def handle_state_change(
hass: HomeAssistant,
config_entry: ConfigEntry,
changed_entity: str,
old_state: State,
new_state: State,
) -> None:
"""Listener to track state changes to sensor entities."""
manager = hass.data[DOMAIN][config_entry.entry_id][MANAGER]
grid_sensor = config_entry.data.get(CONF_GRID)
solar_sensor = config_entry.data.get(CONF_SOLAR)

solar = hass.states.get(solar_sensor).state
grid = hass.states.get(grid_sensor).state

if solar in [None, "unavailable"]:
solar = 0
else:
solar = round(float(hass.states.get(solar_sensor).state))
if grid in [None, "unavailable"]:
grid = 0
else:
grid = round(float(hass.states.get(grid_sensor).state))

invert = config_entry.data.get(CONF_INVERT)

if changed_entity in [grid_sensor, solar_sensor]:
_LOGGER.debug(
"Sending sensor data to OpenEVSE: (solar: %s) (grid: %s)", solar, grid
)
await manager.self_production(grid=grid, solar=solar, invert=invert)


async def homeassistant_started_listener(
hass: HomeAssistant,
config_entry: ConfigEntry,
sensors: list,
evt: Event = None,
):
"""Start tracking state changes after HomeAssistant has started."""
# Listen to sensor state changes so we can fire an event
hass.data[DOMAIN][config_entry.entry_id][UNSUB_LISTENERS].append(
async_track_state_change(
hass,
sensors,
functools.partial(handle_state_change, hass, config_entry),
)
)


async def async_setup( # pylint: disable-next=unused-argument
hass: HomeAssistant, config: Config
) -> bool:
Expand Down Expand Up @@ -65,6 +126,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
COORDINATOR: coordinator,
MANAGER: manager,
FW_COORDINATOR: fw_coordinator,
UNSUB_LISTENERS: [],
}

model_info, sw_version = await get_firmware(manager)
Expand Down Expand Up @@ -100,6 +162,22 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
services = OpenEVSEServices(hass, config_entry)
services.async_register()

sensors = []
if config_entry.data.get(CONF_GRID) and config_entry.data.get(CONF_SOLAR):
sensors.append(config_entry.data.get(CONF_GRID))
sensors.append(config_entry.data.get(CONF_SOLAR))

if len(sensors) > 0:
if hass.state == CoreState.running:
await homeassistant_started_listener(hass, config_entry, sensors)
else:
hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STARTED,
functools.partial(
homeassistant_started_listener, hass, config_entry, sensors
),
)

return True


Expand Down Expand Up @@ -145,6 +223,12 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
await manager.ws_disconnect()

if unload_ok:
# Unsubscribe to any listeners
for unsub_listener in hass.data[DOMAIN][config_entry.entry_id].get(
UNSUB_LISTENERS, []
):
unsub_listener()
hass.data[DOMAIN][config_entry.entry_id].get(UNSUB_LISTENERS, []).clear()
_LOGGER.debug("Successfully removed entities from the %s integration", DOMAIN)
hass.data[DOMAIN].pop(config_entry.entry_id)

Expand Down
15 changes: 14 additions & 1 deletion custom_components/openevse/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,16 @@
from homeassistant.util import slugify
from openevsehttp.__main__ import OpenEVSE

from .const import CONF_NAME, CONF_SERIAL, DEFAULT_HOST, DEFAULT_NAME, DOMAIN
from .const import (
CONF_GRID,
CONF_INVERT,
CONF_NAME,
CONF_SERIAL,
CONF_SOLAR,
DEFAULT_HOST,
DEFAULT_NAME,
DOMAIN,
)

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -180,6 +189,10 @@ def _get_default(key: str, fallback_default: Any = None) -> None:
vol.Optional(
CONF_PASSWORD, default=_get_default(CONF_PASSWORD, "")
): cv.string,
vol.Optional(CONF_GRID, default=_get_default(CONF_GRID, "")): cv.string,
vol.Optional(CONF_SOLAR, default=_get_default(CONF_SOLAR, "")): cv.string,
vol.Optional(CONF_SOLAR, default=_get_default(CONF_SOLAR, "")): cv.string,
vol.Optional(CONF_INVERT, default=_get_default(CONF_INVERT, False)): bool,
},
)

Expand Down
9 changes: 9 additions & 0 deletions custom_components/openevse/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,19 @@

from .entity import OpenEVSESelectEntityDescription, OpenEVSESwitchEntityDescription

# config flow
CONF_NAME = "name"
CONF_SERIAL = "id"
CONF_TYPE = "type"
CONF_GRID = "grid"
CONF_SOLAR = "solar"
CONF_INVERT = "invert_grid"
DEFAULT_HOST = "openevse.local"
DEFAULT_NAME = "OpenEVSE"

# hass.data attribues
UNSUB_LISTENERS = "unsub_listeners"

DOMAIN = "openevse"
COORDINATOR = "coordinator"
FW_COORDINATOR = "fw_coordinator"
Expand All @@ -47,6 +55,7 @@
SERVICE_SET_OVERRIDE = "set_override"
SERVICE_CLEAR_OVERRIDE = "clear_override"

# attributes
ATTR_DEVICE_ID = "device_id"
ATTR_STATE = "state"
ATTR_CHARGE_CURRENT = "charge_current"
Expand Down
2 changes: 1 addition & 1 deletion custom_components/openevse/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"documentation": "https://github.com/firstof9/openevse/",
"iot_class": "local_push",
"issue_tracker": "https://github.com/firstof9/openevse/issues",
"requirements": ["python-openevse-http==0.1.52"],
"requirements": ["python-openevse-http==0.1.53"],
"version": "0.0.0-dev",
"zeroconf": ["_openevse._tcp.local."]
}
14 changes: 10 additions & 4 deletions custom_components/openevse/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@
"name": "Alias",
"host": "Hostname/IP",
"password": "Password (optional)",
"username": "Username (optional)"
"username": "Username (optional)",
"grid": "Grid import/export sensor (optional)",
"solar": "Solar production sensor (optional)",
"invert_grid": "Invert grid import/export"
},
"description": "Please enter the connection information of OpenEVSE charger.",
"description": "Please enter the connection information of OpenEVSE charger.\nYou can also enter your grid import/export and solar production sensors to push data to OpenEVSE.\n\nIMPORTANT NOTE: OpenEVSE expects negative import and positive export.",
"title": "OpenEVSE Setup"
},
"discovery_confirm": {
Expand All @@ -36,9 +39,12 @@
"name": "Alias",
"host": "Host/IP",
"password": "Password (optional)",
"username": "Username (optional)"
"username": "Username (optional)",
"grid": "Grid import/export sensor (optional)",
"solar": "Solar production sensor (optional)",
"invert_grid": "Invert grid import/export"
},
"description": "Please enter the connection information of OpenEVSE charger.",
"description": "Please enter the connection information of OpenEVSE charger.\nYou can also enter your grid import/export and solar production sensors to push data to OpenEVSE.\n\nIMPORTANT NOTE: OpenEVSE expects negative import and positive export.",
"title": "OpenEVSE Setup"
}
}
Expand Down
14 changes: 10 additions & 4 deletions custom_components/openevse/translations/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@
"name": "Alias",
"host": "Hostname/IP",
"password": "Contraseña (opcional)",
"username": "Usuario (opcional)"
"username": "Usuario (opcional)",
"grid": "Sensor de importación/exportación de red (opcional)",
"solar": "Sensor de producción solar (opcional)",
"invert_grid": "Importación/exportación de cuadrícula inversa"
},
"description": "Por favor, introduce la información de conexión al cargador OpenEVSE.",
"description": "Por favor, introduce la información de conexión al cargador OpenEVSE.\nTambién puede ingresar la importación/exportación de su red y los sensores de producción solar para enviar datos a OpenEVSE.\n\nNOTA IMPORTANTE: OpenEVSE espera una importación negativa y una exportación positiva.",
"title": "Configuración OpenEVSE"
}
}
Expand All @@ -32,9 +35,12 @@
"name": "Alias",
"host": "Host/IP",
"password": "Contraseña (opcional)",
"username": "Usuario (opcional)"
"username": "Usuario (opcional)",
"grid": "Sensor de importación/exportación de red (opcional)",
"solar": "Sensor de producción solar (opcional)",
"invert_grid": "Importación/exportación de cuadrícula inversa"
},
"description": "Por favor, introduce la información de conexión con el cargador OpenEVSE.",
"description": "Por favor, introduce la información de conexión al cargador OpenEVSE.\nTambién puede ingresar la importación/exportación de su red y los sensores de producción solar para enviar datos a OpenEVSE.\n\nNOTA IMPORTANTE: OpenEVSE espera una importación negativa y una exportación positiva.",
"title": "Configuración OpenEVSE"
}
}
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
python-openevse-http==0.1.52
python-openevse-http==0.1.53
6 changes: 6 additions & 0 deletions tests/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
"host": "openevse.test.tld",
"username": "",
"password": "",
"grid": "",
"solar": "",
"invert_grid": False,
},
"user",
"openevse",
Expand All @@ -33,6 +36,9 @@
"host": "openevse.test.tld",
"username": "",
"password": "",
"grid": "",
"solar": "",
"invert_grid": False,
},
),
],
Expand Down

0 comments on commit 29d0d63

Please sign in to comment.