Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 8 additions & 10 deletions custom_components/apsystems_ecu_proxy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,15 +92,18 @@ def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Initialize coordinator."""
self.hass = hass
self.config_entry = config_entry

# Add listener for midnight reset.
self.midnight_tracker_unregister = async_track_utc_time_change(
hass, self.midnight_reset, "0", "0", "0", local=True
)

# Get configuration. If initial data else options.
self.no_update_timeout = int(self.config_entry.data.get("no_update_timeout"))

# Add listener for 0 or None if no update.
self.no_update_timer_unregister = None


async def midnight_reset(self, *args):
"""Send dispatcher message to all listeners to reset."""
_LOGGER.debug("midnight reset")
Expand All @@ -118,7 +121,9 @@ async def setup_socket_servers(self) -> None:

for port in SOCKET_PORTS:
_LOGGER.debug("Creating server for port %s", port)
server = MySocketAPI(host, port, self.async_update_callback, self.config_entry)
server = MySocketAPI(
host, port, self.async_update_callback, self.config_entry
)
await server.start()
self.socket_servers.append(server)

Expand Down Expand Up @@ -233,13 +238,6 @@ def async_update_callback(self, data: dict[str, Any]):
_LOGGER.warning("There was a value or index error")
continue


# Get configuration. If initial data else options.
self.no_update_timeout = int(
self.config_entry.options.get('no_update_timeout',
self.config_entry.data.get('no_update_timeout'))
)

# Start the no update timer with the updated value.
self.no_update_timer_unregister = async_call_later(
self.hass, timedelta(seconds=self.no_update_timeout), self.fire_no_update
Expand Down
20 changes: 12 additions & 8 deletions custom_components/apsystems_ecu_proxy/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
import logging
import re
import traceback
from homeassistant.config_entries import ConfigEntry
from typing import Any

from homeassistant.config_entries import ConfigEntry

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -55,7 +55,9 @@
class MySocketAPI:
"""API class."""

def __init__(self, host: str, port: int, callback: Callable, config_entry: ConfigEntry) -> None:
def __init__(
self, host: str, port: int, callback: Callable, config_entry: ConfigEntry
) -> None:
"""Initialize API."""
self.host = host
self.port = port
Expand All @@ -65,9 +67,13 @@ def __init__(self, host: str, port: int, callback: Callable, config_entry: Confi
self.ecu_mem = {}
self.config_entry = config_entry

self.send_to_ema = self.get_config_value("send_to_ema", bool)
self.message_ignore_age = self.get_config_value("message_ignore_age", int)
self.ema_host = self.get_config_value("ema_host", str)

def get_config_value(self, key, default_type):
return default_type(self.config_entry.options.get(key, self.config_entry.data.get(key)))
"""Get config value."""
return default_type(self.config_entry.data.get(key))

async def start(self) -> bool:
"""Start listening socket server."""
Expand Down Expand Up @@ -116,7 +122,6 @@ async def data_received(
# Send data to EMA and send response from EMA to ECU
# send_to_ema is used to stop sending for testing purposes.
# Get configuration. If initial data else options.
self.send_to_ema = self.get_config_value('send_to_ema', bool)
_LOGGER.debug("Send to EMA = %s", self.send_to_ema)
if self.send_to_ema:
response = await self.send_data_to_ema(self.port, data)
Expand Down Expand Up @@ -147,13 +152,12 @@ async def data_received(
ecu["timestamp"] = datetime.strptime(message[60:74], "%Y%m%d%H%M%S")

# MessageFilter: Ignore old messages.
self.message_ignore_age = self.get_config_value('message_ignore_age', int)
_LOGGER.debug("Message ignore age = %s", self.message_ignore_age)
if (
message_age := (datetime.now() - ecu["timestamp"]).total_seconds()
) > self.message_ignore_age:
_LOGGER.debug(
"Message told old with %s sec.",
"Message told old with %s sec",
int(message_age),
)
return None
Expand Down Expand Up @@ -220,9 +224,9 @@ def msg_slice(start_pos: int, end_pos: int, m: re.Match = m) -> int:
return inverters

async def send_data_to_ema(self, port: int, data: bytes) -> bytes:
self.ema_host = self.get_config_value('ema_host', str)
_LOGGER.debug("EMA host = %s", self.ema_host)
"""Send data over async socket."""
_LOGGER.debug("EMA host = %s", self.ema_host)

reader, writer = await asyncio.open_connection(self.ema_host, port)
writer.write(data)
await writer.drain()
Expand Down
107 changes: 59 additions & 48 deletions custom_components/apsystems_ecu_proxy/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,36 @@
"""Config flow."""

import asyncio
import logging

import voluptuous as vol
import asyncio
from homeassistant import config_entries, exceptions

from homeassistant import config_entries
from homeassistant.core import callback

from .const import DOMAIN, KEYS

_LOGGER = logging.getLogger(__name__)


async def validate_ip(ip_address: str) -> bool:
"""Validate EMA server ip."""
try:
reader, writer = await asyncio.wait_for(
asyncio.open_connection(ip_address, 8995), timeout=3.0
)
# Close the connection neatly
writer.close()
await writer.wait_closed()
except (OSError, TimeoutError):
return False
else:
return True


class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Integration configuration."""

VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH

Expand All @@ -24,71 +44,62 @@ def async_get_options_flow(config_entry):
async def async_step_user(self, user_input=None):
"""First step: Initial set-up of integration options."""
_LOGGER.debug("async_step_user")
schema = vol.Schema({
vol.Required(KEYS[0], default="3.67.1.32"): str,
vol.Required(KEYS[1], default="1800"): str,
vol.Required(KEYS[2], default="300"): str,
vol.Required(KEYS[3], default="660"): str,
vol.Required(KEYS[4], default=True): bool,
})
schema = vol.Schema(
{
vol.Required(KEYS[0], default="3.67.1.32"): str,
vol.Required(KEYS[1], default="1800"): str,
vol.Required(KEYS[2], default="300"): str,
vol.Required(KEYS[3], default="660"): str,
vol.Required(KEYS[4], default=True): bool,
}
)

if user_input is not None:
if await OptionsFlowHandler.validate_ip(user_input["ema_host"]):
return self.async_create_entry(title="APsystems ECU proxy", data=user_input)
else:
return self.async_show_form(
step_id="user",
data_schema=schema,
errors={"base": "Could not connect to the specified EMA host"},
if await validate_ip(user_input["ema_host"]):
return self.async_create_entry(
title="APsystems ECU proxy", data=user_input
)

return self.async_show_form(
step_id="user",
data_schema=schema,
errors={"base": "Could not connect to the specified EMA host"},
)
return self.async_show_form(step_id="user", data_schema=schema)


class OptionsFlowHandler(config_entries.OptionsFlow):
"""Regular change of integration options."""

def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Init options handler."""
self.config_entry = config_entry

async def async_step_init(self, user_input=None):
"""Second step: Altering the integration options."""
errors = {}
current_options = (
self.config_entry.data
if not self.config_entry.options
self.config_entry.data
if not self.config_entry.options
else self.config_entry.options
)
_LOGGER.debug("async_step_init with configuration: %s", current_options)

schema = vol.Schema({
vol.Required(key, default=current_options.get(key)): (
str if key != "send_to_ema" else bool
)
for key in KEYS
})

schema = vol.Schema(
{
vol.Required(key, default=current_options.get(key)): (
str if key != "send_to_ema" else bool
)
for key in KEYS
}
)

if user_input is not None:
if await self.validate_ip(user_input["ema_host"]):
updated_options = current_options.copy()
updated_options.update(user_input)
return self.async_create_entry(title="", data=updated_options)
else:
return self.async_show_form(
step_id="init",
data_schema=schema,
errors={"base": "Could not connect to the specified EMA host"},
if await validate_ip(user_input["ema_host"]):
self.hass.config_entries.async_update_entry(
self.config_entry, data=user_input
)
return self.async_show_form(step_id="init", data_schema=schema)
return self.async_create_entry(title="", data={})

@staticmethod
async def validate_ip(ip_address: str) -> bool:
try:
reader, writer = await asyncio.wait_for(
asyncio.open_connection(ip_address, 8995),
timeout=3.0
)
# Close the connection neatly
writer.close()
await writer.wait_closed()
return True
except (OSError, asyncio.TimeoutError):
return False
errors["base"] = "Could not connect to the specified EMA host"
return self.async_show_form(step_id="init", data_schema=schema, errors=errors)
12 changes: 6 additions & 6 deletions custom_components/apsystems_ecu_proxy/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@

# Config flow schema. These are also translated through associated json translations
KEYS = [
"ema_host",
"message_ignore_age",
"max_stub_interval",
"no_update_timeout",
"send_to_ema"
]
"ema_host",
"message_ignore_age",
"max_stub_interval",
"no_update_timeout",
"send_to_ema",
]


class SummationPeriod(StrEnum):
Expand Down
25 changes: 11 additions & 14 deletions custom_components/apsystems_ecu_proxy/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry

from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT,
UnitOfElectricCurrent,
Expand All @@ -32,7 +31,6 @@
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import dt as dt_util


from .const import (
ATTR_SUMMATION_FACTOR,
ATTR_SUMMATION_PERIOD,
Expand Down Expand Up @@ -323,7 +321,7 @@ def handle_ecu_registration(data: dict[str, Any]):
data=data.get(sensor.parameter), attributes=initial_attribute_values
),
)
sensors.append(APSystemsSensor(sensor, config))
sensors.append(APSystemsSensor(sensor, config, config_entry))
add_entities(sensors)

@callback
Expand Down Expand Up @@ -367,7 +365,7 @@ def handle_inverter_registration(data: dict[str, Any]):
data=data.get(sensor.parameter), attributes=initial_attribute_values
),
)
sensors.append(APSystemsSensor(sensor, config))
sensors.append(APSystemsSensor(sensor, config, config_entry))

# Add Inverter channel sensors
for channel in range(data.get("channel_qty", 0)):
Expand All @@ -388,7 +386,7 @@ def handle_inverter_registration(data: dict[str, Any]):
),
name=f"{sensor.name} Ch {channel + 1}",
)
sensors.append(APSystemsSensor(sensor, config))
sensors.append(APSystemsSensor(sensor, config, config_entry))

add_entities(sensors)

Expand Down Expand Up @@ -419,10 +417,10 @@ class APSystemsSensor(RestoreSensor, SensorEntity):
_attr_extra_state_attributes = {}

def __init__(
self,
definition: APSystemSensorDefinition,
self,
definition: APSystemSensorDefinition,
config: APSystemSensorConfig,
config_entry: ConfigEntry # Accept ConfigEntry to get dynamic config values
config_entry: ConfigEntry, # Accept ConfigEntry to get dynamic config values
) -> None:
"""Initialise sensor."""
self._definition = definition
Expand All @@ -437,6 +435,8 @@ def __init__(
self._attr_state_class = definition.state_class
self._attr_unique_id = self._config.unique_id

self.max_stub_interval = int(self.config_entry.data.get("max_stub_interval"))

@property
def is_summation_sensor(self) -> bool:
"""Is this a summation sensor."""
Expand Down Expand Up @@ -646,8 +646,8 @@ def summation_calculation(
current_value: float,
value: float,
) -> int | float:

"""Return summation value of value over time.

If change in period, calculates a value over time from start of new period with
max of MAX_STUB_INTERVAL.
If no change in period, assumes value persisted since last timestamp.
Expand All @@ -669,12 +669,9 @@ def summation_calculation(

sum_value = None
has_changed = False

# Get configuration. If initial data else options.
self.max_stub_interval = int(
self.config_entry.options.get('max_stub_interval',
self.config_entry.data.get('max_stub_interval'))
)

_LOGGER.debug("Max stub interval = %s", self.max_stub_interval)

# Has it crossed calculation period boundry?
Expand Down
Loading