Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New libbi functionality #553

Merged
merged 13 commits into from
Aug 18, 2024
Merged
23 changes: 22 additions & 1 deletion custom_components/myenergi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
from pymyenergi.client import MyenergiClient
from pymyenergi.connection import Connection

from .const import CONF_APP_EMAIL
from .const import CONF_APP_PASSWORD
from .const import CONF_PASSWORD
from .const import CONF_SCAN_INTERVAL
from .const import CONF_USERNAME
Expand All @@ -42,8 +44,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):

username = entry.data.get(CONF_USERNAME)
password = entry.data.get(CONF_PASSWORD)
app_email = entry.data.get(CONF_APP_EMAIL)
app_password = entry.data.get(CONF_APP_PASSWORD)

conn = await hass.async_add_executor_job(
Connection, username, password, app_password, app_email
)
if app_email and app_password:
await conn.discoverLocations()

conn = Connection(username, password)
client = MyenergiClient(conn)

coordinator = MyenergiDataUpdateCoordinator(hass, client=client, entry=entry)
Expand All @@ -59,6 +68,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
)

entry.add_update_listener(async_reload_entry)

# once we reconfigure the integration, we (possibly) need to use the new credentials
entry.async_on_unload(entry.add_update_listener(config_update_listener))

return True


Expand All @@ -76,6 +89,7 @@ def __init__(self, hass: HomeAssistant, client: MyenergiClient, entry) -> None:
entry.data.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL.total_seconds()),
)
)

super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=scan_interval)

async def _async_update_data(self):
Expand All @@ -87,6 +101,9 @@ async def _async_update_data(self):
f"Refresh history local start of day in UTC {utc_today} {utc_today.tzinfo}"
)
try:
await self.hass.async_add_executor_job(
self.client._connection.checkAndUpdateToken
)
await self.client.refresh()
await self.client.refresh_history(utc_today, 24, "hour")
except Exception as exception:
Expand Down Expand Up @@ -115,3 +132,7 @@ async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Reload config entry."""
await async_unload_entry(hass, entry)
await async_setup_entry(hass, entry)


async def config_update_listener(hass, entry):
"""Handle options update."""
46 changes: 42 additions & 4 deletions custom_components/myenergi/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
from pymyenergi.exceptions import WrongCredentials

from . import SCAN_INTERVAL
from .const import CONF_APP_EMAIL
from .const import CONF_APP_PASSWORD
from .const import CONF_PASSWORD
from .const import CONF_SCAN_INTERVAL
from .const import CONF_USERNAME
Expand All @@ -37,7 +39,10 @@ async def async_step_user(self, user_input=None):

if user_input is not None:
err, client = await self._test_credentials(
user_input[CONF_USERNAME], user_input[CONF_PASSWORD]
user_input[CONF_USERNAME],
user_input[CONF_PASSWORD],
user_input[CONF_APP_EMAIL],
user_input[CONF_APP_PASSWORD],
)
if client:
return self.async_create_entry(title=client.site_name, data=user_input)
Expand All @@ -54,23 +59,36 @@ def async_get_options_flow(config_entry):

async def _show_config_form(self, user_input): # pylint: disable=unused-argument
"""Show the configuration form to edit location data."""
defaults = user_input or {CONF_USERNAME: "", CONF_PASSWORD: ""}
defaults = user_input or {
CONF_USERNAME: "",
CONF_PASSWORD: "",
CONF_APP_EMAIL: "",
CONF_APP_PASSWORD: "",
}
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_USERNAME, default=defaults[CONF_USERNAME]): str,
vol.Required(CONF_PASSWORD, default=defaults[CONF_PASSWORD]): str,
vol.Optional(CONF_APP_EMAIL, default=defaults[CONF_APP_EMAIL]): str,
vol.Optional(
CONF_APP_PASSWORD, default=defaults[CONF_APP_PASSWORD]
): str,
}
),
errors=self._errors,
)

async def _test_credentials(self, username, password):
async def _test_credentials(self, username, password, app_email, app_password):
"""Return true if credentials is valid."""
_LOGGER.debug("Test myenergi credentials")
try:
conn = Connection(username, password)
conn = await self.hass.async_add_executor_job(
Connection, username, password, app_password, app_email
)
if app_password and app_email:
await conn.discoverLocations()
client = MyenergiClient(conn)
await client.refresh()
return None, client
Expand Down Expand Up @@ -102,12 +120,30 @@ async def async_step_init(self, user_input=None): # pylint: disable=unused-argu
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
if user_input is not None:
# create a new dict to update config data (don't duplicate it in options)
cdata = {}
cdata[CONF_USERNAME] = self.config_entry.data[CONF_USERNAME]
cdata[CONF_PASSWORD] = self.config_entry.data[CONF_PASSWORD]
if CONF_APP_EMAIL in user_input:
cdata[CONF_APP_EMAIL] = user_input[CONF_APP_EMAIL]
del user_input[CONF_APP_EMAIL]
if CONF_APP_PASSWORD in user_input:
cdata[CONF_APP_PASSWORD] = user_input[CONF_APP_PASSWORD]
del user_input[CONF_APP_PASSWORD]

# now update config data
self.hass.config_entries.async_update_entry(self.config_entry, data=cdata)

# finally update options data (which is now only the SCAN_INTERVAL)
self.options.update(user_input)
return await self._update_options()

scan_interval = self.config_entry.options.get(
CONF_SCAN_INTERVAL, SCAN_INTERVAL.total_seconds()
)
app_email = self.config_entry.options.get(CONF_APP_EMAIL)
app_password = self.config_entry.options.get(CONF_APP_PASSWORD)

return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
Expand All @@ -117,6 +153,8 @@ async def async_step_user(self, user_input=None):
): NumberSelector(
NumberSelectorConfig(min=1, max=300, step=1),
),
vol.Optional(CONF_APP_EMAIL, default=app_email): str,
vol.Optional(CONF_APP_PASSWORD, default=app_password): str,
}
),
)
Expand Down
7 changes: 5 additions & 2 deletions custom_components/myenergi/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
NAME = "myenergi"
DOMAIN = "myenergi"
DOMAIN_DATA = f"{DOMAIN}_data"
VERSION = "0.0.27"
VERSION = "0.0.28-pre"

ATTRIBUTION = "Data provided by myenergi"
ISSUE_URL = "https://github.com/CJNE/ha-myenergi/issues"
Expand All @@ -16,13 +16,16 @@
BINARY_SENSOR = "binary_sensor"
SELECT = "select"
NUMBER = "number"
PLATFORMS = [SENSOR, BINARY_SENSOR, SELECT, NUMBER]
SWITCH = "switch"
PLATFORMS = [SENSOR, BINARY_SENSOR, SELECT, NUMBER, SWITCH]


# Configuration and options
CONF_SCAN_INTERVAL = "scan_interval"
CONF_USERNAME = "username"
CONF_PASSWORD = "password"
CONF_APP_EMAIL = "app_email"
CONF_APP_PASSWORD = "app_password"

# Defaults
DEFAULT_NAME = DOMAIN
Expand Down
20 changes: 20 additions & 0 deletions custom_components/myenergi/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,21 @@ def __init__(self, coordinator, device, config_entry, meta=None):
if self.meta.get("category", None) is not None:
self.meta["category"] = EntityCategory(self.meta["category"])

# async def async_added_to_hass(self):
# """Run when about to be added to hass."""
# async_dispatcher_connect(
# # The Hass Object
# self.hass,
# # The Signal to listen for.
# # Try to make it unique per entity instance
# # so include something like entity_id
# # or other unique data from the service call
# self.entity_id,
# # Function handle to call when signal is received
# self.libbi_set_charge_target
# )
# _LOGGER.debug("registered signal with HA")
#
@property
def device_info(self):
return {
Expand Down Expand Up @@ -74,6 +89,11 @@ async def unlock(self) -> None:
_LOGGER.debug("unlock called")
"""Unlock"""
await self.device.unlock()

async def libbi_set_charge_target(self, chargetarget: float) -> None:
_LOGGER.debug("Setting libbi charge target to %s Wh", chargetarget)
"""Set libbi charge target"""
await self.device.set_charge_target(chargetarget)
self.schedule_update_ha_state()


Expand Down
4 changes: 2 additions & 2 deletions custom_components/myenergi/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
"documentation": "https://github.com/cjne/ha-myenergi",
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/cjne/ha-myenergi/issues",
"requirements": ["pymyenergi==0.1.1"],
"version": "0.0.27"
"requirements": ["pymyenergi==0.2.0"],
"version": "0.0.28-pre"
}
18 changes: 15 additions & 3 deletions custom_components/myenergi/select.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@
from .const import DOMAIN
from .entity import MyenergiEntity

LIBBI_MODE_NAMES = {0: "Stopped", 1: "Normal", 5: "Export"}
LIBBI_MODE_NAMES = {"STOP": "Stopped", "BALANCE": "Normal", "DRAIN": "Export"}

ATTR_BOOST_AMOUNT = "amount"
ATTR_BOOST_TIME = "time"
ATTR_BOOST_TARGET = "target"
ATTR_BOOST_WHEN = "when"
ATTR_CHARGE_TARGET = "chargetarget"
BOOST_SCHEMA = {
vol.Required(ATTR_BOOST_AMOUNT): vol.All(
vol.Coerce(float),
Expand All @@ -33,6 +34,12 @@
),
vol.Required(ATTR_BOOST_WHEN): str,
}
LIBBI_CHARGE_TARGET_SCHEMA = {
vol.Required(ATTR_CHARGE_TARGET): vol.All(
vol.Coerce(float),
vol.Range(min=0, max=20400),
)
}


async def async_setup_entry(hass, entry, async_add_devices):
Expand Down Expand Up @@ -73,7 +80,13 @@ async def async_setup_entry(hass, entry, async_add_devices):
"start_eddi_boost",
)
devices.append(EddiOperatingModeSelect(coordinator, device, entry))
# libbi services and selects
elif device.kind == "libbi":
platform.async_register_entity_service(
"myenergi_libbi_charge_target",
LIBBI_CHARGE_TARGET_SCHEMA,
"libbi_set_charge_target",
)
devices.append(LibbiOperatingModeSelect(coordinator, device, entry))
async_add_devices(devices)

Expand Down Expand Up @@ -170,8 +183,7 @@ def name(self):
@property
def current_option(self):
"""Return the state of the sensor."""
mode = self.device.local_mode
return LIBBI_MODE_NAMES[mode]
return self.device.get_mode_description(self.device.local_mode)

async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
Expand Down
21 changes: 20 additions & 1 deletion custom_components/myenergi/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,22 @@ async def async_setup_entry(hass, entry, async_add_devices):
),
)
)
sensors.append(
MyenergiSensor(
coordinator,
device,
entry,
create_meta(
"Charge target",
"charge_target",
SensorDeviceClass.ENERGY,
UnitOfEnergy.KILO_WATT_HOUR,
None,
None,
SensorStateClass.MEASUREMENT,
),
)
)
async_add_devices(sensors)


Expand Down Expand Up @@ -740,7 +756,10 @@ def name(self):
def state(self):
"""Return the state of the sensor."""
value = operator.attrgetter(self.meta["prop_name"])(self.device)
return value
if value is not None:
return value
else:
self._attr_available = False

@property
def unit_of_measurement(self):
Expand Down
16 changes: 16 additions & 0 deletions custom_components/myenergi/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,19 @@ myenergi_unlock:
model: Zappi
entity:
domain: select
myenergi_libbi_charge_target:
name: Charge target
description: Set charge target for libbi
target:
device:
model: Libbi
fields:
chargetarget:
name: Charge Target
description: Libbi charge target in Wh
required: true
selector:
number:
min: 0
max: 20400
step: 255
Loading
Loading