-
-
Notifications
You must be signed in to change notification settings - Fork 30.3k
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
Add OSO Energy integration #70365
Add OSO Energy integration #70365
Changes from all commits
2eb0db0
f442086
9a3e0e6
7af68d2
1d5ba58
3c4843f
3a24b08
9befade
bda3176
5192d2d
ac51123
88e7d5d
d188f7e
ee7828d
3acfcea
50ace86
ee3d69e
c53d8be
5c6c760
d0245e4
12217db
bf233f8
15bf6e7
ca207e3
52e998b
cd67848
0543afc
bae6dbb
d1b79af
8840296
6d4edff
71e3711
72412d8
d20c24a
dc93b29
1ac0b29
a1bec70
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,81 @@ | ||||||
"""Support for the OSO Energy devices and services.""" | ||||||
from typing import Any, Generic, TypeVar | ||||||
|
||||||
from aiohttp.web_exceptions import HTTPException | ||||||
from apyosoenergyapi import OSOEnergy | ||||||
from apyosoenergyapi.helper.const import ( | ||||||
OSOEnergyBinarySensorData, | ||||||
OSOEnergySensorData, | ||||||
OSOEnergyWaterHeaterData, | ||||||
) | ||||||
from apyosoenergyapi.helper.osoenergy_exceptions import OSOEnergyReauthRequired | ||||||
|
||||||
from homeassistant.config_entries import ConfigEntry | ||||||
from homeassistant.const import CONF_API_KEY, Platform | ||||||
from homeassistant.core import HomeAssistant | ||||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady | ||||||
from homeassistant.helpers import aiohttp_client | ||||||
from homeassistant.helpers.entity import Entity | ||||||
|
||||||
from .const import DOMAIN | ||||||
|
||||||
_T = TypeVar( | ||||||
"_T", OSOEnergyBinarySensorData, OSOEnergySensorData, OSOEnergyWaterHeaterData | ||||||
) | ||||||
|
||||||
PLATFORMS = [ | ||||||
Platform.WATER_HEATER, | ||||||
] | ||||||
PLATFORM_LOOKUP = { | ||||||
Platform.WATER_HEATER: "water_heater", | ||||||
} | ||||||
|
||||||
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||||||
"""Set up OSO Energy from a config entry.""" | ||||||
subscription_key = entry.data[CONF_API_KEY] | ||||||
websession = aiohttp_client.async_get_clientsession(hass) | ||||||
osoenergy = OSOEnergy(subscription_key, websession) | ||||||
|
||||||
osoenergy_config = dict(entry.data) | ||||||
|
||||||
hass.data.setdefault(DOMAIN, {}) | ||||||
|
||||||
try: | ||||||
devices: Any = await osoenergy.session.start_session(osoenergy_config) | ||||||
except HTTPException as error: | ||||||
raise ConfigEntryNotReady() from error | ||||||
except OSOEnergyReauthRequired as err: | ||||||
raise ConfigEntryAuthFailed from err | ||||||
|
||||||
osohotwateriot marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
hass.data[DOMAIN][entry.entry_id] = osoenergy | ||||||
|
||||||
platforms = set() | ||||||
for ha_type, oso_type in PLATFORM_LOOKUP.items(): | ||||||
osohotwateriot marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
device_list = devices.get(oso_type, []) | ||||||
if device_list: | ||||||
platforms.add(ha_type) | ||||||
if platforms: | ||||||
await hass.config_entries.async_forward_entry_setups(entry, platforms) | ||||||
return True | ||||||
|
||||||
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||||||
"""Unload a config entry.""" | ||||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will error if the platform wasn't setup. |
||||||
if unload_ok: | ||||||
hass.data[DOMAIN].pop(entry.entry_id) | ||||||
|
||||||
return unload_ok | ||||||
|
||||||
|
||||||
class OSOEnergyEntity(Entity, Generic[_T]): | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
"""Initiate OSO Energy Base Class.""" | ||||||
|
||||||
_attr_has_entity_name = True | ||||||
|
||||||
def __init__(self, osoenergy: OSOEnergy, osoenergy_device: _T) -> None: | ||||||
"""Initialize the instance.""" | ||||||
self.osoenergy = osoenergy | ||||||
self.device = osoenergy_device | ||||||
self._attr_unique_id = osoenergy_device.device_id |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
"""Config Flow for OSO Energy.""" | ||
from collections.abc import Mapping | ||
import logging | ||
from typing import Any | ||
|
||
from apyosoenergyapi import OSOEnergy | ||
import voluptuous as vol | ||
|
||
from homeassistant import config_entries | ||
from homeassistant.config_entries import ConfigEntry | ||
from homeassistant.const import CONF_API_KEY | ||
from homeassistant.data_entry_flow import FlowResult | ||
from homeassistant.helpers import aiohttp_client | ||
|
||
from .const import DOMAIN | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
_SCHEMA_STEP_USER = vol.Schema({vol.Required(CONF_API_KEY): str}) | ||
|
||
osohotwateriot marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
class OSOEnergyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): | ||
"""Handle a OSO Energy config flow.""" | ||
|
||
VERSION = 1 | ||
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is legacy. It's set in the manifest.json nowadays. |
||
|
||
def __init__(self) -> None: | ||
"""Initialize.""" | ||
self.entry: ConfigEntry | None = None | ||
|
||
async def async_step_user(self, user_input=None) -> FlowResult: | ||
"""Handle a flow initialized by the user.""" | ||
errors = {} | ||
|
||
if user_input is not None: | ||
# Verify Subscription key | ||
if user_email := await self.get_user_email(user_input[CONF_API_KEY]): | ||
await self.async_set_unique_id(user_email) | ||
|
||
if ( | ||
self.context["source"] == config_entries.SOURCE_REAUTH | ||
and self.entry | ||
): | ||
self.hass.config_entries.async_update_entry( | ||
self.entry, title=user_email, data=user_input | ||
) | ||
await self.hass.config_entries.async_reload(self.entry.entry_id) | ||
return self.async_abort(reason="reauth_successful") | ||
|
||
self._abort_if_unique_id_configured() | ||
return self.async_create_entry(title=user_email, data=user_input) | ||
osohotwateriot marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
errors["base"] = "invalid_auth" | ||
|
||
return self.async_show_form( | ||
step_id="user", | ||
data_schema=_SCHEMA_STEP_USER, | ||
errors=errors, | ||
) | ||
|
||
async def get_user_email(self, subscription_key: str) -> str | None: | ||
"""Return the user email for the provided subscription key.""" | ||
try: | ||
websession = aiohttp_client.async_get_clientsession(self.hass) | ||
client = OSOEnergy(subscription_key, websession) | ||
return await client.get_user_email() | ||
except Exception: # pylint: disable=broad-except | ||
_LOGGER.exception("Unknown error occurred") | ||
return None | ||
|
||
async def async_step_reauth(self, user_input: Mapping[str, Any]) -> FlowResult: | ||
"""Re Authenticate a user.""" | ||
self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) | ||
data = {CONF_API_KEY: user_input[CONF_API_KEY]} | ||
osohotwateriot marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return await self.async_step_user(data) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We shouldn't pass the old config entry data as input to the user step. Then we'll try to authenticate again with the old data when we know it's already invalid. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
"""Constants for OSO Energy.""" | ||
|
||
DOMAIN = "osoenergy" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"domain": "osoenergy", | ||
"name": "OSO Energy", | ||
"codeowners": ["@osohotwateriot"], | ||
"config_flow": true, | ||
"documentation": "https://www.home-assistant.io/integrations/osoenergy", | ||
"iot_class": "cloud_polling", | ||
"requirements": ["pyosoenergyapi==1.1.3"] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
{ | ||
"config": { | ||
"step": { | ||
"user": { | ||
"title": "OSO Energy Auth", | ||
"description": "Enter the generated 'Subscription Key' for your account at 'https://portal.osoenergy.no/'", | ||
"data": { | ||
"api_key": "[%key:common::config_flow::data::api_key%]" | ||
} | ||
}, | ||
"reauth": { | ||
"title": "OSO Energy Auth", | ||
"description": "Generate and enter a new 'Subscription Key' for your account at 'https://portal.osoenergy.no/'.", | ||
"data": { | ||
"api_key": "[%key:common::config_flow::data::api_key%]" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's no form for the reauth step. |
||
} | ||
} | ||
}, | ||
"error": { | ||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cannot_connect string isn't used. |
||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", | ||
"unknown": "[%key:common::config_flow::error::unknown%]" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. unknown string isn't used. |
||
}, | ||
"abort": { | ||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]", | ||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
"""Support for OSO Energy water heaters.""" | ||
Danielhiversen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
from typing import Any | ||
|
||
from apyosoenergyapi.helper.const import OSOEnergyWaterHeaterData | ||
|
||
from homeassistant.components.water_heater import ( | ||
STATE_ECO, | ||
STATE_ELECTRIC, | ||
STATE_HIGH_DEMAND, | ||
STATE_OFF, | ||
WaterHeaterEntity, | ||
WaterHeaterEntityFeature, | ||
) | ||
from homeassistant.config_entries import ConfigEntry | ||
from homeassistant.const import UnitOfTemperature | ||
from homeassistant.core import HomeAssistant | ||
from homeassistant.helpers.device_registry import DeviceInfo | ||
from homeassistant.helpers.entity_platform import AddEntitiesCallback | ||
|
||
from . import OSOEnergyEntity | ||
from .const import DOMAIN | ||
|
||
CURRENT_OPERATION_MAP: dict[str, Any] = { | ||
"default": { | ||
"off": STATE_OFF, | ||
"powersave": STATE_OFF, | ||
"extraenergy": STATE_HIGH_DEMAND, | ||
}, | ||
"oso": { | ||
"auto": STATE_ECO, | ||
"off": STATE_OFF, | ||
"powersave": STATE_OFF, | ||
"extraenergy": STATE_HIGH_DEMAND, | ||
}, | ||
} | ||
HEATER_MIN_TEMP = 10 | ||
HEATER_MAX_TEMP = 80 | ||
MANUFACTURER = "OSO Energy" | ||
|
||
|
||
async def async_setup_entry( | ||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback | ||
) -> None: | ||
"""Set up OSO Energy heater based on a config entry.""" | ||
osoenergy = hass.data[DOMAIN][entry.entry_id] | ||
devices = osoenergy.session.device_list.get("water_heater") | ||
entities = [] | ||
if devices: | ||
for dev in devices: | ||
entities.append(OSOEnergyWaterHeater(osoenergy, dev)) | ||
async_add_entities(entities, True) | ||
|
||
|
||
class OSOEnergyWaterHeater( | ||
OSOEnergyEntity[OSOEnergyWaterHeaterData], WaterHeaterEntity | ||
): | ||
"""OSO Energy Water Heater Device.""" | ||
|
||
_attr_name = None | ||
_attr_supported_features = WaterHeaterEntityFeature.TARGET_TEMPERATURE | ||
_attr_temperature_unit = UnitOfTemperature.CELSIUS | ||
|
||
@property | ||
def device_info(self) -> DeviceInfo: | ||
"""Return device information.""" | ||
return DeviceInfo( | ||
identifiers={(DOMAIN, self.device.device_id)}, | ||
manufacturer=MANUFACTURER, | ||
model=self.device.device_type, | ||
name=self.device.device_name, | ||
) | ||
|
||
@property | ||
def available(self) -> bool: | ||
"""Return if the device is available.""" | ||
return self.device.available | ||
|
||
@property | ||
def current_operation(self) -> str: | ||
"""Return current operation.""" | ||
status = self.device.current_operation | ||
if status == "off": | ||
return STATE_OFF | ||
|
||
optimization_mode = self.device.optimization_mode.lower() | ||
heater_mode = self.device.heater_mode.lower() | ||
if optimization_mode in CURRENT_OPERATION_MAP: | ||
return CURRENT_OPERATION_MAP[optimization_mode].get( | ||
heater_mode, STATE_ELECTRIC | ||
) | ||
|
||
return CURRENT_OPERATION_MAP["default"].get(heater_mode, STATE_ELECTRIC) | ||
|
||
@property | ||
def current_temperature(self) -> float: | ||
"""Return the current temperature of the heater.""" | ||
return self.device.current_temperature | ||
|
||
@property | ||
def target_temperature(self) -> float: | ||
"""Return the temperature we try to reach.""" | ||
return self.device.target_temperature | ||
|
||
@property | ||
def target_temperature_high(self) -> float: | ||
"""Return the temperature we try to reach.""" | ||
return self.device.target_temperature_high | ||
|
||
@property | ||
def target_temperature_low(self) -> float: | ||
"""Return the temperature we try to reach.""" | ||
return self.device.target_temperature_low | ||
|
||
@property | ||
def min_temp(self) -> float: | ||
"""Return the minimum temperature.""" | ||
return self.device.min_temperature | ||
|
||
@property | ||
def max_temp(self) -> float: | ||
"""Return the maximum temperature.""" | ||
return self.device.max_temperature | ||
|
||
async def async_turn_on(self, **kwargs) -> None: | ||
"""Turn on hotwater.""" | ||
await self.osoenergy.hotwater.turn_on(self.device, True) | ||
|
||
async def async_turn_off(self, **kwargs) -> None: | ||
"""Turn off hotwater.""" | ||
await self.osoenergy.hotwater.turn_off(self.device, True) | ||
|
||
async def async_set_temperature(self, **kwargs: Any) -> None: | ||
"""Set new target temperature.""" | ||
target_temperature = int(kwargs.get("temperature", self.target_temperature)) | ||
profile = [target_temperature] * 24 | ||
|
||
await self.osoenergy.hotwater.set_profile(self.device, profile) | ||
|
||
async def async_update(self) -> None: | ||
"""Update all Node data from Hive.""" | ||
await self.osoenergy.session.update_data() | ||
self.device = await self.osoenergy.hotwater.get_water_heater(self.device) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -343,6 +343,7 @@ | |
"openweathermap", | ||
"opower", | ||
"oralb", | ||
"osoenergy", | ||
"otbr", | ||
"overkiz", | ||
"ovo_energy", | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's give this
TypeVar
a better name, maybe: