mirror of
https://github.com/home-assistant/core.git
synced 2025-07-18 02:37:08 +00:00
Add OSO Energy services (#118770)
* Add OSO Energy services * Fixes after review * Add tests for OSO Energy water heater * Fixes after review * Revert changes for service schema in OSO Energy * Improve osoenergy unit tests
This commit is contained in:
parent
d40341f1ad
commit
cdf809926b
@ -11,5 +11,22 @@
|
|||||||
"default": "mdi:water-boiler"
|
"default": "mdi:water-boiler"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"services": {
|
||||||
|
"get_profile": {
|
||||||
|
"service": "mdi:thermometer-lines"
|
||||||
|
},
|
||||||
|
"set_profile": {
|
||||||
|
"service": "mdi:thermometer-lines"
|
||||||
|
},
|
||||||
|
"set_v40_min": {
|
||||||
|
"service": "mdi:car-coolant-level"
|
||||||
|
},
|
||||||
|
"turn_off": {
|
||||||
|
"service": "mdi:water-boiler-off"
|
||||||
|
},
|
||||||
|
"turn_on": {
|
||||||
|
"service": "mdi:water-boiler"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
261
homeassistant/components/osoenergy/services.yaml
Normal file
261
homeassistant/components/osoenergy/services.yaml
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
get_profile:
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
domain: water_heater
|
||||||
|
set_profile:
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
domain: water_heater
|
||||||
|
fields:
|
||||||
|
hour_00:
|
||||||
|
required: false
|
||||||
|
example: 75
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 10
|
||||||
|
max: 75
|
||||||
|
step: 1
|
||||||
|
unit_of_measurement: °C
|
||||||
|
hour_01:
|
||||||
|
required: false
|
||||||
|
example: 75
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 10
|
||||||
|
max: 75
|
||||||
|
step: 1
|
||||||
|
unit_of_measurement: °C
|
||||||
|
hour_02:
|
||||||
|
required: false
|
||||||
|
example: 75
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 10
|
||||||
|
max: 75
|
||||||
|
step: 1
|
||||||
|
unit_of_measurement: °C
|
||||||
|
hour_03:
|
||||||
|
required: false
|
||||||
|
example: 75
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 10
|
||||||
|
max: 75
|
||||||
|
step: 1
|
||||||
|
unit_of_measurement: °C
|
||||||
|
hour_04:
|
||||||
|
required: false
|
||||||
|
example: 75
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 10
|
||||||
|
max: 75
|
||||||
|
step: 1
|
||||||
|
unit_of_measurement: °C
|
||||||
|
hour_05:
|
||||||
|
required: false
|
||||||
|
example: 75
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 10
|
||||||
|
max: 75
|
||||||
|
step: 1
|
||||||
|
unit_of_measurement: °C
|
||||||
|
hour_06:
|
||||||
|
required: false
|
||||||
|
example: 75
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 10
|
||||||
|
max: 75
|
||||||
|
step: 1
|
||||||
|
unit_of_measurement: °C
|
||||||
|
hour_07:
|
||||||
|
required: false
|
||||||
|
example: 75
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 10
|
||||||
|
max: 75
|
||||||
|
step: 1
|
||||||
|
unit_of_measurement: °C
|
||||||
|
hour_08:
|
||||||
|
required: false
|
||||||
|
example: 75
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 10
|
||||||
|
max: 75
|
||||||
|
step: 1
|
||||||
|
unit_of_measurement: °C
|
||||||
|
hour_09:
|
||||||
|
required: false
|
||||||
|
example: 75
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 10
|
||||||
|
max: 75
|
||||||
|
step: 1
|
||||||
|
unit_of_measurement: °C
|
||||||
|
hour_10:
|
||||||
|
required: false
|
||||||
|
example: 75
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 10
|
||||||
|
max: 75
|
||||||
|
step: 1
|
||||||
|
unit_of_measurement: °C
|
||||||
|
hour_11:
|
||||||
|
required: false
|
||||||
|
example: 75
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 10
|
||||||
|
max: 75
|
||||||
|
step: 1
|
||||||
|
unit_of_measurement: °C
|
||||||
|
hour_12:
|
||||||
|
required: false
|
||||||
|
example: 75
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 10
|
||||||
|
max: 75
|
||||||
|
step: 1
|
||||||
|
unit_of_measurement: °C
|
||||||
|
hour_13:
|
||||||
|
required: false
|
||||||
|
example: 75
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 10
|
||||||
|
max: 75
|
||||||
|
step: 1
|
||||||
|
unit_of_measurement: °C
|
||||||
|
hour_14:
|
||||||
|
required: false
|
||||||
|
example: 75
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 10
|
||||||
|
max: 75
|
||||||
|
step: 1
|
||||||
|
unit_of_measurement: °C
|
||||||
|
hour_15:
|
||||||
|
required: false
|
||||||
|
example: 75
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 10
|
||||||
|
max: 75
|
||||||
|
step: 1
|
||||||
|
unit_of_measurement: °C
|
||||||
|
hour_16:
|
||||||
|
required: false
|
||||||
|
example: 75
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 10
|
||||||
|
max: 75
|
||||||
|
step: 1
|
||||||
|
unit_of_measurement: °C
|
||||||
|
hour_17:
|
||||||
|
required: false
|
||||||
|
example: 75
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 10
|
||||||
|
max: 75
|
||||||
|
step: 1
|
||||||
|
unit_of_measurement: °C
|
||||||
|
hour_18:
|
||||||
|
required: false
|
||||||
|
example: 75
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 10
|
||||||
|
max: 75
|
||||||
|
step: 1
|
||||||
|
unit_of_measurement: °C
|
||||||
|
hour_19:
|
||||||
|
required: false
|
||||||
|
example: 75
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 10
|
||||||
|
max: 75
|
||||||
|
step: 1
|
||||||
|
unit_of_measurement: °C
|
||||||
|
hour_20:
|
||||||
|
required: false
|
||||||
|
example: 75
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 10
|
||||||
|
max: 75
|
||||||
|
step: 1
|
||||||
|
unit_of_measurement: °C
|
||||||
|
hour_21:
|
||||||
|
required: false
|
||||||
|
example: 75
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 10
|
||||||
|
max: 75
|
||||||
|
step: 1
|
||||||
|
unit_of_measurement: °C
|
||||||
|
hour_22:
|
||||||
|
required: false
|
||||||
|
example: 75
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 10
|
||||||
|
max: 75
|
||||||
|
step: 1
|
||||||
|
unit_of_measurement: °C
|
||||||
|
hour_23:
|
||||||
|
required: false
|
||||||
|
example: 75
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 10
|
||||||
|
max: 75
|
||||||
|
step: 1
|
||||||
|
unit_of_measurement: °C
|
||||||
|
set_v40_min:
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
domain: water_heater
|
||||||
|
fields:
|
||||||
|
v40_min:
|
||||||
|
required: true
|
||||||
|
example: 240
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 200
|
||||||
|
max: 550
|
||||||
|
step: 1
|
||||||
|
unit_of_measurement: L
|
||||||
|
turn_off:
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
domain: water_heater
|
||||||
|
fields:
|
||||||
|
until_temp_limit:
|
||||||
|
required: true
|
||||||
|
default: false
|
||||||
|
example: false
|
||||||
|
selector:
|
||||||
|
boolean:
|
||||||
|
turn_on:
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
domain: water_heater
|
||||||
|
fields:
|
||||||
|
until_temp_limit:
|
||||||
|
required: true
|
||||||
|
default: false
|
||||||
|
example: false
|
||||||
|
selector:
|
||||||
|
boolean:
|
@ -91,5 +91,143 @@
|
|||||||
"name": "Temperature one"
|
"name": "Temperature one"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"services": {
|
||||||
|
"get_profile": {
|
||||||
|
"name": "Get heater profile",
|
||||||
|
"description": "Get the temperature profile of water heater"
|
||||||
|
},
|
||||||
|
"set_profile": {
|
||||||
|
"name": "Set heater profile",
|
||||||
|
"description": "Set the temperature profile of water heater",
|
||||||
|
"fields": {
|
||||||
|
"hour_00": {
|
||||||
|
"name": "00:00",
|
||||||
|
"description": "00:00 hour"
|
||||||
|
},
|
||||||
|
"hour_01": {
|
||||||
|
"name": "01:00",
|
||||||
|
"description": "01:00 hour"
|
||||||
|
},
|
||||||
|
"hour_02": {
|
||||||
|
"name": "02:00",
|
||||||
|
"description": "02:00 hour"
|
||||||
|
},
|
||||||
|
"hour_03": {
|
||||||
|
"name": "03:00",
|
||||||
|
"description": "03:00 hour"
|
||||||
|
},
|
||||||
|
"hour_04": {
|
||||||
|
"name": "04:00",
|
||||||
|
"description": "04:00 hour"
|
||||||
|
},
|
||||||
|
"hour_05": {
|
||||||
|
"name": "05:00",
|
||||||
|
"description": "05:00 hour"
|
||||||
|
},
|
||||||
|
"hour_06": {
|
||||||
|
"name": "06:00",
|
||||||
|
"description": "06:00 hour"
|
||||||
|
},
|
||||||
|
"hour_07": {
|
||||||
|
"name": "07:00",
|
||||||
|
"description": "07:00 hour"
|
||||||
|
},
|
||||||
|
"hour_08": {
|
||||||
|
"name": "08:00",
|
||||||
|
"description": "08:00 hour"
|
||||||
|
},
|
||||||
|
"hour_09": {
|
||||||
|
"name": "09:00",
|
||||||
|
"description": "09:00 hour"
|
||||||
|
},
|
||||||
|
"hour_10": {
|
||||||
|
"name": "10:00",
|
||||||
|
"description": "10:00 hour"
|
||||||
|
},
|
||||||
|
"hour_11": {
|
||||||
|
"name": "11:00",
|
||||||
|
"description": "11:00 hour"
|
||||||
|
},
|
||||||
|
"hour_12": {
|
||||||
|
"name": "12:00",
|
||||||
|
"description": "12:00 hour"
|
||||||
|
},
|
||||||
|
"hour_13": {
|
||||||
|
"name": "13:00",
|
||||||
|
"description": "13:00 hour"
|
||||||
|
},
|
||||||
|
"hour_14": {
|
||||||
|
"name": "14:00",
|
||||||
|
"description": "14:00 hour"
|
||||||
|
},
|
||||||
|
"hour_15": {
|
||||||
|
"name": "15:00",
|
||||||
|
"description": "15:00 hour"
|
||||||
|
},
|
||||||
|
"hour_16": {
|
||||||
|
"name": "16:00",
|
||||||
|
"description": "16:00 hour"
|
||||||
|
},
|
||||||
|
"hour_17": {
|
||||||
|
"name": "17:00",
|
||||||
|
"description": "17:00 hour"
|
||||||
|
},
|
||||||
|
"hour_18": {
|
||||||
|
"name": "18:00",
|
||||||
|
"description": "18:00 hour"
|
||||||
|
},
|
||||||
|
"hour_19": {
|
||||||
|
"name": "19:00",
|
||||||
|
"description": "19:00 hour"
|
||||||
|
},
|
||||||
|
"hour_20": {
|
||||||
|
"name": "20:00",
|
||||||
|
"description": "20:00 hour"
|
||||||
|
},
|
||||||
|
"hour_21": {
|
||||||
|
"name": "21:00",
|
||||||
|
"description": "21:00 hour"
|
||||||
|
},
|
||||||
|
"hour_22": {
|
||||||
|
"name": "22:00",
|
||||||
|
"description": "22:00 hour"
|
||||||
|
},
|
||||||
|
"hour_23": {
|
||||||
|
"name": "23:00",
|
||||||
|
"description": "23:00 hour"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"set_v40_min": {
|
||||||
|
"name": "Set v40 min",
|
||||||
|
"description": "Set the minimum quantity of water at 40°C for a heater",
|
||||||
|
"fields": {
|
||||||
|
"v40_min": {
|
||||||
|
"name": "V40 Min",
|
||||||
|
"description": "Minimum quantity of water at 40°C (200-350 for SAGA S200, 300-550 for SAGA S300)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"turn_off": {
|
||||||
|
"name": "Turn off heating",
|
||||||
|
"description": "Turn off heating for one hour or until min temperature is reached",
|
||||||
|
"fields": {
|
||||||
|
"until_temp_limit": {
|
||||||
|
"name": "Until temperature limit",
|
||||||
|
"description": "Choose if heating should be off until min temperature (True) is reached or for one hour (False)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"turn_on": {
|
||||||
|
"name": "Turn on heating",
|
||||||
|
"description": "Turn on heating for one hour or until max temperature is reached",
|
||||||
|
"fields": {
|
||||||
|
"until_temp_limit": {
|
||||||
|
"name": "Until temperature limit",
|
||||||
|
"description": "Choose if heating should be on until max temperature (True) is reached or for one hour (False)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
"""Support for OSO Energy water heaters."""
|
"""Support for OSO Energy water heaters."""
|
||||||
|
|
||||||
|
import datetime as dt
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from apyosoenergyapi import OSOEnergy
|
from apyosoenergyapi import OSOEnergy
|
||||||
from apyosoenergyapi.helper.const import OSOEnergyWaterHeaterData
|
from apyosoenergyapi.helper.const import OSOEnergyWaterHeaterData
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.water_heater import (
|
from homeassistant.components.water_heater import (
|
||||||
STATE_ECO,
|
STATE_ECO,
|
||||||
@ -15,12 +17,17 @@ from homeassistant.components.water_heater import (
|
|||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import UnitOfTemperature
|
from homeassistant.const import UnitOfTemperature
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant, ServiceResponse, SupportsResponse
|
||||||
|
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
import homeassistant.util.dt as dt_util
|
||||||
|
from homeassistant.util.json import JsonValueType
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .entity import OSOEnergyEntity
|
from .entity import OSOEnergyEntity
|
||||||
|
|
||||||
|
ATTR_UNTIL_TEMP_LIMIT = "until_temp_limit"
|
||||||
|
ATTR_V40MIN = "v40_min"
|
||||||
CURRENT_OPERATION_MAP: dict[str, Any] = {
|
CURRENT_OPERATION_MAP: dict[str, Any] = {
|
||||||
"default": {
|
"default": {
|
||||||
"off": STATE_OFF,
|
"off": STATE_OFF,
|
||||||
@ -34,6 +41,11 @@ CURRENT_OPERATION_MAP: dict[str, Any] = {
|
|||||||
"extraenergy": STATE_HIGH_DEMAND,
|
"extraenergy": STATE_HIGH_DEMAND,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
SERVICE_GET_PROFILE = "get_profile"
|
||||||
|
SERVICE_SET_PROFILE = "set_profile"
|
||||||
|
SERVICE_SET_V40MIN = "set_v40_min"
|
||||||
|
SERVICE_TURN_OFF = "turn_off"
|
||||||
|
SERVICE_TURN_ON = "turn_on"
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
@ -46,6 +58,102 @@ async def async_setup_entry(
|
|||||||
return
|
return
|
||||||
async_add_entities((OSOEnergyWaterHeater(osoenergy, dev) for dev in devices), True)
|
async_add_entities((OSOEnergyWaterHeater(osoenergy, dev) for dev in devices), True)
|
||||||
|
|
||||||
|
platform = entity_platform.async_get_current_platform()
|
||||||
|
|
||||||
|
platform.async_register_entity_service(
|
||||||
|
SERVICE_GET_PROFILE,
|
||||||
|
{},
|
||||||
|
OSOEnergyWaterHeater.async_get_profile.__name__,
|
||||||
|
supports_response=SupportsResponse.ONLY,
|
||||||
|
)
|
||||||
|
|
||||||
|
service_set_profile_schema = cv.make_entity_service_schema(
|
||||||
|
{
|
||||||
|
vol.Optional(f"hour_{hour:02d}"): vol.All(
|
||||||
|
vol.Coerce(int), vol.Range(min=10, max=75)
|
||||||
|
)
|
||||||
|
for hour in range(24)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
platform.async_register_entity_service(
|
||||||
|
SERVICE_SET_PROFILE,
|
||||||
|
service_set_profile_schema,
|
||||||
|
OSOEnergyWaterHeater.async_set_profile.__name__,
|
||||||
|
)
|
||||||
|
|
||||||
|
platform.async_register_entity_service(
|
||||||
|
SERVICE_SET_V40MIN,
|
||||||
|
{
|
||||||
|
vol.Required(ATTR_V40MIN): vol.All(
|
||||||
|
vol.Coerce(float), vol.Range(min=200, max=550)
|
||||||
|
),
|
||||||
|
},
|
||||||
|
OSOEnergyWaterHeater.async_set_v40_min.__name__,
|
||||||
|
)
|
||||||
|
|
||||||
|
platform.async_register_entity_service(
|
||||||
|
SERVICE_TURN_OFF,
|
||||||
|
{vol.Required(ATTR_UNTIL_TEMP_LIMIT): vol.All(cv.boolean)},
|
||||||
|
OSOEnergyWaterHeater.async_oso_turn_off.__name__,
|
||||||
|
)
|
||||||
|
|
||||||
|
platform.async_register_entity_service(
|
||||||
|
SERVICE_TURN_ON,
|
||||||
|
{vol.Required(ATTR_UNTIL_TEMP_LIMIT): vol.All(cv.boolean)},
|
||||||
|
OSOEnergyWaterHeater.async_oso_turn_on.__name__,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_utc_hour(local_hour: int) -> dt.datetime:
|
||||||
|
"""Convert the requested local hour to a utc hour for the day.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
local_hour: the local hour (0-23) for the current day to be converted.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Datetime representation for the requested hour in utc time for the day.
|
||||||
|
|
||||||
|
"""
|
||||||
|
now = dt_util.now()
|
||||||
|
local_time = now.replace(hour=local_hour, minute=0, second=0, microsecond=0)
|
||||||
|
return dt_util.as_utc(local_time)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_local_hour(utc_hour: int) -> dt.datetime:
|
||||||
|
"""Convert the requested utc hour to a local hour for the day.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
utc_hour: the utc hour (0-23) for the current day to be converted.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Datetime representation for the requested hour in local time for the day.
|
||||||
|
|
||||||
|
"""
|
||||||
|
utc_now = dt_util.utcnow()
|
||||||
|
utc_time = utc_now.replace(hour=utc_hour, minute=0, second=0, microsecond=0)
|
||||||
|
return dt_util.as_local(utc_time)
|
||||||
|
|
||||||
|
|
||||||
|
def _convert_profile_to_local(values: list[float]) -> list[JsonValueType]:
|
||||||
|
"""Convert UTC profile to local.
|
||||||
|
|
||||||
|
Receives a device temperature schedule - 24 values for the day where the index represents the hour of the day in UTC.
|
||||||
|
Converts the schedule to local time.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
values: list of floats representing the 24 hour temperature schedule for the device
|
||||||
|
Returns:
|
||||||
|
The device temperature schedule in local time.
|
||||||
|
|
||||||
|
"""
|
||||||
|
profile: list[JsonValueType] = [0.0] * 24
|
||||||
|
for hour in range(24):
|
||||||
|
local_hour = _get_local_hour(hour)
|
||||||
|
profile[local_hour.hour] = float(values[hour])
|
||||||
|
|
||||||
|
return profile
|
||||||
|
|
||||||
|
|
||||||
class OSOEnergyWaterHeater(
|
class OSOEnergyWaterHeater(
|
||||||
OSOEnergyEntity[OSOEnergyWaterHeaterData], WaterHeaterEntity
|
OSOEnergyEntity[OSOEnergyWaterHeaterData], WaterHeaterEntity
|
||||||
@ -53,7 +161,9 @@ class OSOEnergyWaterHeater(
|
|||||||
"""OSO Energy Water Heater Device."""
|
"""OSO Energy Water Heater Device."""
|
||||||
|
|
||||||
_attr_name = None
|
_attr_name = None
|
||||||
_attr_supported_features = WaterHeaterEntityFeature.TARGET_TEMPERATURE
|
_attr_supported_features = (
|
||||||
|
WaterHeaterEntityFeature.TARGET_TEMPERATURE | WaterHeaterEntityFeature.ON_OFF
|
||||||
|
)
|
||||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -131,6 +241,36 @@ class OSOEnergyWaterHeater(
|
|||||||
|
|
||||||
await self.osoenergy.hotwater.set_profile(self.entity_data, profile)
|
await self.osoenergy.hotwater.set_profile(self.entity_data, profile)
|
||||||
|
|
||||||
|
async def async_get_profile(self) -> ServiceResponse:
|
||||||
|
"""Return the current temperature profile of the device."""
|
||||||
|
|
||||||
|
profile = self.entity_data.profile
|
||||||
|
return {"profile": _convert_profile_to_local(profile)}
|
||||||
|
|
||||||
|
async def async_set_profile(self, **kwargs: Any) -> None:
|
||||||
|
"""Handle the service call."""
|
||||||
|
profile = self.entity_data.profile
|
||||||
|
|
||||||
|
for hour in range(24):
|
||||||
|
hour_key = f"hour_{hour:02d}"
|
||||||
|
|
||||||
|
if hour_key in kwargs:
|
||||||
|
profile[_get_utc_hour(hour).hour] = kwargs[hour_key]
|
||||||
|
|
||||||
|
await self.osoenergy.hotwater.set_profile(self.entity_data, profile)
|
||||||
|
|
||||||
|
async def async_set_v40_min(self, v40_min) -> None:
|
||||||
|
"""Handle the service call."""
|
||||||
|
await self.osoenergy.hotwater.set_v40_min(self.entity_data, v40_min)
|
||||||
|
|
||||||
|
async def async_oso_turn_off(self, until_temp_limit) -> None:
|
||||||
|
"""Handle the service call."""
|
||||||
|
await self.osoenergy.hotwater.turn_off(self.entity_data, until_temp_limit)
|
||||||
|
|
||||||
|
async def async_oso_turn_on(self, until_temp_limit) -> None:
|
||||||
|
"""Handle the service call."""
|
||||||
|
await self.osoenergy.hotwater.turn_on(self.entity_data, until_temp_limit)
|
||||||
|
|
||||||
async def async_update(self) -> None:
|
async def async_update(self) -> None:
|
||||||
"""Update all Node data from Hive."""
|
"""Update all Node data from Hive."""
|
||||||
await self.osoenergy.session.update_data()
|
await self.osoenergy.session.update_data()
|
||||||
|
90
tests/components/osoenergy/conftest.py
Normal file
90
tests/components/osoenergy/conftest.py
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
"""Common fixtures for the OSO Energy tests."""
|
||||||
|
|
||||||
|
from collections.abc import Generator
|
||||||
|
from typing import Any
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
|
from apyosoenergyapi.waterheater import OSOEnergyWaterHeaterData
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.osoenergy.const import DOMAIN
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import CONF_API_KEY
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.util.json import JsonObjectType
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, load_json_object_fixture
|
||||||
|
|
||||||
|
MOCK_CONFIG = {
|
||||||
|
CONF_API_KEY: "secret_api_key",
|
||||||
|
}
|
||||||
|
TEST_USER_EMAIL = "test_user_email@domain.com"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def water_heater_fixture() -> JsonObjectType:
|
||||||
|
"""Load the water heater fixture."""
|
||||||
|
return load_json_object_fixture("water_heater.json", DOMAIN)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_water_heater(water_heater_fixture) -> MagicMock:
|
||||||
|
"""Water heater mock object."""
|
||||||
|
mock_heater = MagicMock(OSOEnergyWaterHeaterData)
|
||||||
|
for key, value in water_heater_fixture.items():
|
||||||
|
setattr(mock_heater, key, value)
|
||||||
|
return mock_heater
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_entry_data() -> dict[str, Any]:
|
||||||
|
"""Mock config entry data for fixture."""
|
||||||
|
return MOCK_CONFIG
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_config_entry(
|
||||||
|
hass: HomeAssistant, mock_entry_data: dict[str, Any]
|
||||||
|
) -> ConfigEntry:
|
||||||
|
"""Mock a config entry setup for incomfort integration."""
|
||||||
|
entry = MockConfigEntry(domain=DOMAIN, data=mock_entry_data)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
return entry
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def mock_osoenergy_client(mock_water_heater) -> Generator[AsyncMock]:
|
||||||
|
"""Mock a OSO Energy client."""
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.osoenergy.OSOEnergy", MagicMock()
|
||||||
|
) as mock_client,
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.osoenergy.config_flow.OSOEnergy", new=mock_client
|
||||||
|
),
|
||||||
|
):
|
||||||
|
mock_session = MagicMock()
|
||||||
|
mock_session.device_list = {"water_heater": [mock_water_heater]}
|
||||||
|
mock_session.start_session = AsyncMock(
|
||||||
|
return_value={"water_heater": [mock_water_heater]}
|
||||||
|
)
|
||||||
|
mock_session.update_data = AsyncMock(return_value=True)
|
||||||
|
|
||||||
|
mock_client().session = mock_session
|
||||||
|
|
||||||
|
mock_hotwater = MagicMock()
|
||||||
|
mock_hotwater.get_water_heater = AsyncMock(return_value=mock_water_heater)
|
||||||
|
mock_hotwater.set_profile = AsyncMock(return_value=True)
|
||||||
|
mock_hotwater.set_v40_min = AsyncMock(return_value=True)
|
||||||
|
mock_hotwater.turn_on = AsyncMock(return_value=True)
|
||||||
|
mock_hotwater.turn_off = AsyncMock(return_value=True)
|
||||||
|
|
||||||
|
mock_client().hotwater = mock_hotwater
|
||||||
|
|
||||||
|
mock_client().get_user_email = AsyncMock(return_value=TEST_USER_EMAIL)
|
||||||
|
mock_client().start_session = AsyncMock(
|
||||||
|
return_value={"water_heater": [mock_water_heater]}
|
||||||
|
)
|
||||||
|
|
||||||
|
yield mock_client
|
20
tests/components/osoenergy/fixtures/water_heater.json
Normal file
20
tests/components/osoenergy/fixtures/water_heater.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"device_id": "osoenergy_water_heater",
|
||||||
|
"device_type": "SAGA S200",
|
||||||
|
"device_name": "TEST DEVICE",
|
||||||
|
"current_temperature": 60,
|
||||||
|
"min_temperature": 10,
|
||||||
|
"max_temperature": 75,
|
||||||
|
"target_temperature": 60,
|
||||||
|
"target_temperature_low": 57,
|
||||||
|
"target_temperature_high": 63,
|
||||||
|
"available": true,
|
||||||
|
"online": true,
|
||||||
|
"current_operation": "on",
|
||||||
|
"optimization_mode": "oso",
|
||||||
|
"heater_mode": "auto",
|
||||||
|
"profile": [
|
||||||
|
10, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60,
|
||||||
|
60, 60, 60, 60, 60
|
||||||
|
]
|
||||||
|
}
|
57
tests/components/osoenergy/snapshots/test_water_heater.ambr
Normal file
57
tests/components/osoenergy/snapshots/test_water_heater.ambr
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
# serializer version: 1
|
||||||
|
# name: test_water_heater[water_heater.test_device-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': dict({
|
||||||
|
'max_temp': 75,
|
||||||
|
'min_temp': 10,
|
||||||
|
}),
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'water_heater',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'water_heater.test_device',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
}),
|
||||||
|
'original_device_class': None,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': None,
|
||||||
|
'platform': 'osoenergy',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'supported_features': <WaterHeaterEntityFeature: 9>,
|
||||||
|
'translation_key': None,
|
||||||
|
'unique_id': 'osoenergy_water_heater',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_water_heater[water_heater.test_device-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'current_temperature': 60,
|
||||||
|
'friendly_name': 'TEST DEVICE',
|
||||||
|
'max_temp': 75,
|
||||||
|
'min_temp': 10,
|
||||||
|
'supported_features': <WaterHeaterEntityFeature: 9>,
|
||||||
|
'target_temp_high': 63,
|
||||||
|
'target_temp_low': 57,
|
||||||
|
'temperature': 60,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'water_heater.test_device',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': 'eco',
|
||||||
|
})
|
||||||
|
# ---
|
276
tests/components/osoenergy/test_water_heater.py
Normal file
276
tests/components/osoenergy/test_water_heater.py
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
"""The water heater tests for the OSO Energy platform."""
|
||||||
|
|
||||||
|
from unittest.mock import ANY, MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from syrupy import SnapshotAssertion
|
||||||
|
|
||||||
|
from homeassistant.components.osoenergy.const import DOMAIN
|
||||||
|
from homeassistant.components.osoenergy.water_heater import (
|
||||||
|
ATTR_UNTIL_TEMP_LIMIT,
|
||||||
|
ATTR_V40MIN,
|
||||||
|
SERVICE_GET_PROFILE,
|
||||||
|
SERVICE_SET_PROFILE,
|
||||||
|
SERVICE_SET_V40MIN,
|
||||||
|
)
|
||||||
|
from homeassistant.components.water_heater import (
|
||||||
|
DOMAIN as WATER_HEATER_DOMAIN,
|
||||||
|
SERVICE_SET_TEMPERATURE,
|
||||||
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_ENTITY_ID,
|
||||||
|
ATTR_TEMPERATURE,
|
||||||
|
SERVICE_TURN_OFF,
|
||||||
|
SERVICE_TURN_ON,
|
||||||
|
Platform,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
|
from tests.common import snapshot_platform
|
||||||
|
|
||||||
|
|
||||||
|
@patch("homeassistant.components.osoenergy.PLATFORMS", [Platform.WATER_HEATER])
|
||||||
|
async def test_water_heater(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
mock_osoenergy_client: MagicMock,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
mock_config_entry: ConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test states of the water heater."""
|
||||||
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.freeze_time("2024-10-10 00:00:00")
|
||||||
|
async def test_get_profile(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_osoenergy_client: MagicMock,
|
||||||
|
mock_config_entry: ConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test getting the heater profile."""
|
||||||
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
profile = await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_GET_PROFILE,
|
||||||
|
{ATTR_ENTITY_ID: "water_heater.test_device"},
|
||||||
|
blocking=True,
|
||||||
|
return_response=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# The profile is returned in UTC format from the server
|
||||||
|
# Each index represents an hour from the current day (0-23). For example index 2 - 02:00 UTC
|
||||||
|
# Depending on the time zone and the DST the UTC hour is converted to local time and the value is placed in the correct index
|
||||||
|
# Example: time zone 'US/Pacific' and DST (-7 hours difference) - index 9 (09:00 UTC) will be converted to index 2 (02:00 Local)
|
||||||
|
assert profile == {
|
||||||
|
"water_heater.test_device": {
|
||||||
|
"profile": [
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
10,
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.freeze_time("2024-10-10 00:00:00")
|
||||||
|
async def test_set_profile(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_osoenergy_client: MagicMock,
|
||||||
|
mock_config_entry: ConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test getting the heater profile."""
|
||||||
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_PROFILE,
|
||||||
|
{ATTR_ENTITY_ID: "water_heater.test_device", "hour_01": 45},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# The server expects to receive the profile in UTC format
|
||||||
|
# Each field represents an hour from the current day (0-23). For example field hour_01 - 01:00 Local time
|
||||||
|
# Depending on the time zone and the DST the Local hour is converted to UTC time and the value is placed in the correct index
|
||||||
|
# Example: time zone 'US/Pacific' and DST (-7 hours difference) - index 1 (01:00 Local) will be converted to index 8 (08:00 Utc)
|
||||||
|
mock_osoenergy_client().hotwater.set_profile.assert_called_once_with(
|
||||||
|
ANY,
|
||||||
|
[
|
||||||
|
10,
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
45,
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_set_v40_min(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_osoenergy_client: MagicMock,
|
||||||
|
mock_config_entry: ConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test getting the heater profile."""
|
||||||
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_V40MIN,
|
||||||
|
{ATTR_ENTITY_ID: "water_heater.test_device", ATTR_V40MIN: 300},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_osoenergy_client().hotwater.set_v40_min.assert_called_once_with(ANY, 300)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_set_temperature(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_osoenergy_client: MagicMock,
|
||||||
|
mock_config_entry: ConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test getting the heater profile."""
|
||||||
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
await hass.services.async_call(
|
||||||
|
WATER_HEATER_DOMAIN,
|
||||||
|
SERVICE_SET_TEMPERATURE,
|
||||||
|
{ATTR_ENTITY_ID: "water_heater.test_device", ATTR_TEMPERATURE: 45},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_osoenergy_client().hotwater.set_profile.assert_called_once_with(
|
||||||
|
ANY,
|
||||||
|
[
|
||||||
|
45,
|
||||||
|
45,
|
||||||
|
45,
|
||||||
|
45,
|
||||||
|
45,
|
||||||
|
45,
|
||||||
|
45,
|
||||||
|
45,
|
||||||
|
45,
|
||||||
|
45,
|
||||||
|
45,
|
||||||
|
45,
|
||||||
|
45,
|
||||||
|
45,
|
||||||
|
45,
|
||||||
|
45,
|
||||||
|
45,
|
||||||
|
45,
|
||||||
|
45,
|
||||||
|
45,
|
||||||
|
45,
|
||||||
|
45,
|
||||||
|
45,
|
||||||
|
45,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_turn_on(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_osoenergy_client: MagicMock,
|
||||||
|
mock_config_entry: ConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test turning the heater on."""
|
||||||
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
await hass.services.async_call(
|
||||||
|
WATER_HEATER_DOMAIN,
|
||||||
|
SERVICE_TURN_ON,
|
||||||
|
{ATTR_ENTITY_ID: "water_heater.test_device"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_osoenergy_client().hotwater.turn_on.assert_called_once_with(ANY, True)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_turn_off(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_osoenergy_client: MagicMock,
|
||||||
|
mock_config_entry: ConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test getting the heater profile."""
|
||||||
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
await hass.services.async_call(
|
||||||
|
WATER_HEATER_DOMAIN,
|
||||||
|
SERVICE_TURN_OFF,
|
||||||
|
{ATTR_ENTITY_ID: "water_heater.test_device"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_osoenergy_client().hotwater.turn_off.assert_called_once_with(ANY, True)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_oso_turn_on(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_osoenergy_client: MagicMock,
|
||||||
|
mock_config_entry: ConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test turning the heater on."""
|
||||||
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_TURN_ON,
|
||||||
|
{ATTR_ENTITY_ID: "water_heater.test_device", ATTR_UNTIL_TEMP_LIMIT: False},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_osoenergy_client().hotwater.turn_on.assert_called_once_with(ANY, False)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_oso_turn_off(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_osoenergy_client: MagicMock,
|
||||||
|
mock_config_entry: ConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test getting the heater profile."""
|
||||||
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_TURN_OFF,
|
||||||
|
{ATTR_ENTITY_ID: "water_heater.test_device", ATTR_UNTIL_TEMP_LIMIT: False},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_osoenergy_client().hotwater.turn_off.assert_called_once_with(ANY, False)
|
Loading…
x
Reference in New Issue
Block a user