Update custom component 'powercalc' and adjusted config accordingly

This commit is contained in:
Burningstone91
2021-09-07 18:18:43 +02:00
parent 25222e26b7
commit 0a13dc4c7d
97 changed files with 973 additions and 272 deletions

View File

@@ -2,23 +2,35 @@
from __future__ import annotations
import logging
import re
from datetime import timedelta
from typing import Optional
import homeassistant.helpers.config_validation as cv
import voluptuous as vol
from homeassistant.components.utility_meter.const import (
DAILY,
METER_TYPES,
MONTHLY,
WEEKLY,
)
from homeassistant.const import CONF_SCAN_INTERVAL
from homeassistant.helpers.typing import HomeAssistantType
from .const import (
CONF_CREATE_ENERGY_SENSORS,
CONF_CREATE_UTILITY_METERS,
CONF_ENERGY_SENSOR_NAMING,
CONF_ENTITY_NAME_PATTERN,
CONF_FIXED,
CONF_LINEAR,
CONF_MAX_POWER,
CONF_MAX_WATT,
CONF_MIN_POWER,
CONF_MIN_WATT,
CONF_POWER,
CONF_POWER_SENSOR_NAMING,
CONF_STATES_POWER,
CONF_WATT,
CONF_UTILITY_METER_TYPES,
DATA_CALCULATOR_FACTORY,
DOMAIN,
DOMAIN_CONFIG,
MODE_FIXED,
MODE_LINEAR,
MODE_LUT,
@@ -30,12 +42,60 @@ from .strategy_interface import PowerCalculationStrategyInterface
from .strategy_linear import LinearStrategy
from .strategy_lut import LutRegistry, LutStrategy
_LOGGER = logging.getLogger(__name__)
DEFAULT_SCAN_INTERVAL = timedelta(minutes=10)
DEFAULT_POWER_NAME_PATTERN = "{} power"
DEFAULT_ENERGY_NAME_PATTERN = "{} energy"
def validate_name_pattern(value: str) -> str:
"""Validate that the naming pattern contains {}."""
regex = re.compile(r"\{\}")
if not regex.search(value):
raise vol.Invalid("Naming pattern must contain {}")
return value
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.All(
vol.Schema(
{
vol.Optional(
CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL
): cv.time_period,
vol.Optional(CONF_ENTITY_NAME_PATTERN): validate_name_pattern,
vol.Optional(
CONF_POWER_SENSOR_NAMING, default=DEFAULT_POWER_NAME_PATTERN
): validate_name_pattern,
vol.Optional(
CONF_ENERGY_SENSOR_NAMING, default=DEFAULT_ENERGY_NAME_PATTERN
): validate_name_pattern,
vol.Optional(CONF_CREATE_ENERGY_SENSORS, default=True): cv.boolean,
vol.Optional(CONF_CREATE_UTILITY_METERS, default=False): cv.boolean,
vol.Optional(
CONF_UTILITY_METER_TYPES, default=[DAILY, WEEKLY, MONTHLY]
): vol.All(cv.ensure_list, [vol.In(METER_TYPES)]),
}
),
)
},
extra=vol.ALLOW_EXTRA,
)
async def async_setup(hass: HomeAssistantType, config: dict) -> bool:
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][DATA_CALCULATOR_FACTORY] = PowerCalculatorStrategyFactory(hass)
conf = config.get(DOMAIN) or {
CONF_POWER_SENSOR_NAMING: DEFAULT_POWER_NAME_PATTERN,
CONF_ENERGY_SENSOR_NAMING: DEFAULT_ENERGY_NAME_PATTERN,
CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL,
CONF_CREATE_ENERGY_SENSORS: True,
CONF_CREATE_UTILITY_METERS: False,
}
hass.data[DOMAIN] = {
DATA_CALCULATOR_FACTORY: PowerCalculatorStrategyFactory(hass),
DOMAIN_CONFIG: conf,
}
return True
@@ -70,19 +130,8 @@ class PowerCalculatorStrategyFactory:
"""Create the linear strategy"""
linear_config = config.get(CONF_LINEAR)
if linear_config is None:
# Below is for BC compatibility
if config.get(CONF_MIN_WATT) is not None:
_LOGGER.warning(
"min_watt is deprecated and will be removed in version 0.3, use linear->min_power"
)
linear_config = {
CONF_MIN_POWER: config.get(CONF_MIN_WATT),
CONF_MAX_POWER: config.get(CONF_MAX_WATT),
}
elif light_model is not None:
linear_config = light_model.linear_mode_config
if linear_config is None and light_model is not None:
linear_config = light_model.linear_mode_config
return LinearStrategy(linear_config, entity_domain)
@@ -92,13 +141,6 @@ class PowerCalculatorStrategyFactory:
if fixed_config is None and light_model is not None:
fixed_config = light_model.fixed_mode_config
# BC compat
if fixed_config is None:
_LOGGER.warning(
"watt is deprecated and will be removed in version 0.3, use fixed->power"
)
fixed_config = {CONF_POWER: config.get(CONF_WATT)}
return FixedStrategy(
fixed_config.get(CONF_POWER), fixed_config.get(CONF_STATES_POWER)
)

View File

@@ -0,0 +1,15 @@
from __future__ import annotations
from collections.abc import Iterable, Mapping
from typing import Any, NamedTuple
import attr
class SourceEntity(NamedTuple):
unique_id: str
object_id: str
entity_id: str
name: str
domain: str
capabilities: Mapping[str, Any] | None = attr.ib(default=None)

View File

@@ -1,17 +1,26 @@
"""The Hue Power constants."""
DOMAIN = "powercalc"
DOMAIN_CONFIG = "config"
DATA_CALCULATOR_FACTORY = "calculator_factory"
CONF_CALIBRATE = "calibrate"
CONF_CREATE_ENERGY_SENSOR = "create_energy_sensor"
CONF_CREATE_ENERGY_SENSORS = "create_energy_sensors"
CONF_CREATE_UTILITY_METERS = "create_utility_meters"
CONF_ENERGY_SENSOR_NAMING = "energy_sensor_naming"
CONF_ENTITY_NAME_PATTERN = "entity_name_pattern"
CONF_FIXED = "fixed"
CONF_LINEAR = "linear"
CONF_MODEL = "model"
CONF_MANUFACTURER = "manufacturer"
CONF_MODE = "mode"
CONF_MULTIPLY_FACTOR = "multiply_factor"
CONF_MULTIPLY_FACTOR_STANDBY = "multiply_factor_standby"
CONF_MIN_WATT = "min_watt"
CONF_MAX_WATT = "max_watt"
CONF_POWER_SENSOR_NAMING = "power_sensor_naming"
CONF_POWER = "power"
CONF_MIN_POWER = "min_power"
CONF_MAX_POWER = "max_power"
@@ -20,14 +29,22 @@ CONF_STATES_POWER = "states_power"
CONF_STANDBY_USAGE = "standby_usage"
CONF_DISABLE_STANDBY_USAGE = "disable_standby_usage"
CONF_CUSTOM_MODEL_DIRECTORY = "custom_model_directory"
CONF_UTILITY_METER_TYPES = "utility_meter_types"
MODE_LUT = "lut"
MODE_LINEAR = "linear"
MODE_FIXED = "fixed"
CALCULATION_MODES = [
MODE_FIXED,
MODE_LINEAR,
MODE_LUT,
]
MANUFACTURER_DIRECTORY_MAPPING = {
"IKEA of Sweden": "ikea",
"Feibit Inc co. ": "jiawen",
"LEDVANCE": "ledvance",
"MLI": "mueller-licht",
"OSRAM": "osram",
"Signify Netherlands B.V.": "signify",
}
@@ -35,6 +52,8 @@ MANUFACTURER_DIRECTORY_MAPPING = {
MODEL_DIRECTORY_MAPPING = {
"IKEA of Sweden": {
"TRADFRI bulb E14 WS opal 400lm": "LED1536G5",
"TRADFRI bulb GU10 WS 400lm": "LED1537R6",
"TRADFRI bulb E27 WS opal 980lm": "LED1545G12",
"TRADFRI bulb E27 WS clear 950lm": "LED1546G12",
"TRADFRI bulb E27 opal 1000lm": "LED1623G12",
"TRADFRI bulb E27 CWS opal 600lm": "LED1624G9",

View File

@@ -0,0 +1,10 @@
{
"name": "WeMo smart LED bulb",
"standby_usage": 0.4,
"supported_modes": [
"lut"
],
"measure_method": "manual",
"measure_device": "RRPM02 (Model); 4897037690801 (Barcode); Reduction Revolution Plug-in Power Meter",
"measure_description": "Was not able to measure the standby usage, the value is assumed"
}

View File

@@ -3,5 +3,8 @@
"standby_usage": 0.44,
"supported_modes": [
"lut"
]
],
"measure_method": "script",
"measure_device": "Shelly Plug S",
"measure_description": "Measured with script made by bramstroker and partially verified manual"
}

View File

@@ -0,0 +1,10 @@
{
"name": "TRADFRI bulb GU10 WS 400lm LED1537R6",
"standby_usage": 0.44,
"supported_modes": [
"lut"
],
"measure_method": "script",
"measure_device": "Shelly Plug S",
"measure_description": "Measured once with old version script made by bramstroker using 6 sec pauses and partially verified and fixed manual"
}

View File

@@ -0,0 +1,6 @@
{
"name": "TRADFRI bulb E27 WS opal 980lm LED1545G12",
"supported_modes": [
"lut"
]
}

View File

@@ -3,5 +3,8 @@
"standby_usage": 0.43,
"supported_modes": [
"lut"
]
],
"measure_method": "script",
"measure_device": "Shelly Plug S",
"measure_description": "Measured with script made by bramstroker and partially verified manual"
}

View File

@@ -3,5 +3,8 @@
"standby_usage": 0.36,
"supported_modes": [
"lut"
]
],
"measure_method": "manual",
"measure_device": "Power Recorder; PR10-D (Model); Shenzen Zhurui Technology (Manufacturer)",
"measure_description": "Bulbs was measured twice at each dim setting (100 points) and mean value was used when there were differences"
}

View File

@@ -3,5 +3,8 @@
"standby_usage": 0.40,
"supported_modes": [
"lut"
]
],
"measure_method": "manual",
"measure_device": "Power Recorder; PR10-D (Model); Shenzen Zhurui Technology (Manufacturer)",
"measure_description": "Bulbs was measured twice at each dim setting (100 points) and mean value was used when there were differences"
}

View File

@@ -3,5 +3,8 @@
"standby_usage": 0.38,
"supported_modes": [
"lut"
]
],
"measure_method": "manual",
"measure_device": "Power Recorder; PR10-D (Model); Shenzen Zhurui Technology (Manufacturer)",
"measure_description": "Bulbs was measured twice at each dim setting (100 points) and mean value was used when there were differences"
}

View File

@@ -3,5 +3,8 @@
"standby_usage": 0.26,
"supported_modes": [
"lut"
]
],
"measure_method": "script",
"measure_device": "Shelly Plug S",
"measure_description": "Measured with script made by bramstroker and partially verified manual"
}

View File

@@ -3,5 +3,8 @@
"standby_usage": 0.35,
"supported_modes": [
"lut"
]
],
"measure_method": "manual",
"measure_device": "Power Recorder; PR10-D (Model); Shenzen Zhurui Technology (Manufacturer)",
"measure_description": "Bulbs was measured twice at each dim setting (100 points) and mean value was used when there were differences"
}

View File

@@ -3,5 +3,8 @@
"standby_usage": 0.38,
"supported_modes": [
"lut"
]
],
"measure_method": "manual",
"measure_device": "Power Recorder; PR10-D (Model); Shenzen Zhurui Technology (Manufacturer)",
"measure_description": "Bulbs was measured twice at each dim setting (100 points) and mean value was used when there were differences"
}

View File

@@ -0,0 +1,10 @@
{
"name": "Tibea Lamp E27 Tuneable White 2000lm",
"standby_usage": 0.51,
"supported_modes": [
"lut"
],
"measure_method": "script",
"measure_device": "Shelly Plug S",
"measure_description": "Measured twice with script made by bramstroker and partially verified manual"
}

View File

@@ -0,0 +1,64 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "model.json described a light model",
"type": "object",
"required": [
"name",
"standby_usage",
"supported_modes",
"measure_method",
"measure_device"
],
"properties": {
"name": {
"type": "string",
"description": "The full name"
},
"standby_usage": {
"type": "number",
"description": "Power draw when the light is turned of. When you are not able to measure set to 0.4"
},
"supported_modes": {
"type": "array",
"items": {
"type": "string",
"enum": ["lut", "linear", "fixed"]
},
"description": "Supported calculation modes"
},
"measure_method": {
"type": "string",
"enum": ["manual", "script"],
"description": "How the light was measured"
},
"measure_device": {
"type": "string",
"description": "Device which was used to measure"
},
"measure_description": {
"type": "string",
"description": "Add more information about how you measured the light or any remarks"
},
"linear_config": {
"type": "object",
"description": "Configuration for linear calculation mode",
"properties": {
"min_watt": {
"type": "number"
},
"max_watt": {
"type": "number"
}
}
},
"fixed_config": {
"type": "object",
"description": "Configuration for fixed calculation mode",
"properties": {
"watt": {
"type": "number"
}
}
}
}
}

View File

@@ -0,0 +1,10 @@
{
"name": "tint GU10 Spot 350lm",
"standby_usage": 0.31,
"supported_modes": [
"lut"
],
"measure_method": "script",
"measure_device": "Shelly Plug S",
"measure_description": "Measured once with script made by bramstroker using 9 sec pauses and partially verified manual"
}

View File

@@ -0,0 +1,10 @@
{
"name":"LIGHTIFY Indoor Flex RGBW 3P (1.8m)",
"standby_usage":0.3,
"supported_modes":[
"lut"
],
"measure_device":"Gosund SP111 on Tasmota 9.5.0",
"measure_description":"Measure Script for Tasmota",
"measure_method":"script"
}

View File

@@ -1,6 +1,7 @@
{
"name": "Hue White and Color Ambiance A19 E26 (Gen 5)",
"name": "Hue White and Color Ambiance A19 E26/E27 (Gen 5)",
"standby_usage": 0.33,
"supported_modes": [
"lut"
]
}
}

View File

@@ -0,0 +1,10 @@
{
"name": "Hue White and Color Ambiance E14 Candle w/ BT",
"standby_usage": 0.28,
"supported_modes": [
"lut"
],
"measure_method": "script",
"measure_device": "AVM FRITZ!DECT 200",
"measure_description": "Measured with own custom script"
}

View File

@@ -0,0 +1,10 @@
{
"name":"Hue Calla Outdoor Pedestal",
"standby_usage":0.4,
"supported_modes":[
"lut"
],
"measure_device":"Gosund SP111 on Tasmota 9.5.0",
"measure_description":"Measure Script for Tasmota",
"measure_method":"script"
}

View File

@@ -0,0 +1,10 @@
{
"name": "Philips Hue White and Color Ambience (1 gen)",
"supported_modes": [
"lut"
],
"standby_usage": 0.22,
"measure_method": "script",
"measure_device": "Shelly PM1",
"measure_description": "Used HASS based script"
}

View File

@@ -2,5 +2,8 @@
"name": "Hue White and Color Ambiance Spot GU10",
"supported_modes": [
"lut"
]
],
"measure_method": "script",
"measure_device": "Shelly plug S",
"measure_description": "Used script utils/measure_shelly.py from repository"
}

View File

@@ -2,5 +2,8 @@
"name": "Hue White and Color Ambiance A19 E26 (Gen 3)",
"supported_modes": [
"lut"
]
],
"measure_method": "script",
"measure_device": "Shelly plug S",
"measure_description": "Used script utils/measure_shelly.py from repository"
}

View File

@@ -2,5 +2,8 @@
"name": "Hue White and Color Ambiance Candle E12",
"supported_modes": [
"lut"
]
],
"measure_method": "script",
"measure_device": "Shelly plug S",
"measure_description": "Used script utils/measure_shelly.py from repository"
}

View File

@@ -1,6 +1,10 @@
{
"name": "Hue White and Color Ambiance A19 E26 (Gen 4)",
"name": "Hue White and Color Ambiance A19 E26/E27 (Gen 4)",
"standby_usage": 0.47,
"measure_device": "unknown",
"measure_method": "script",
"measure_description": "",
"supported_modes": [
"lut"
]
}
}

View File

@@ -0,0 +1,10 @@
{
"name": "Hue Play",
"standby_usage": 0.100,
"supported_modes": [
"lut"
],
"measure_method": "script",
"measure_device": "TP-Link HS110",
"measure_description": "Measured with script made by smonesi (steps: bri 20/sat 40/mired 30/hue 2000) and linearly interpolated to provide more data"
}

View File

@@ -0,0 +1,10 @@
{
"name": "Hue Go 2.0",
"standby_usage": 0.49,
"measure_method": "script",
"measure_device": "TP-Link HS110",
"measure_description": "Measured with measure_v2 script made by smonesi (SAMPLE_COUNT=6, steps: bri 40/sat 40/mired 30/hue 2000) and linearly interpolated to provide more data",
"supported_modes": [
"lut"
]
}

View File

@@ -0,0 +1,10 @@
{
"name": "Philips Living Color Iris",
"standby_usage": 0.500,
"supported_modes": [
"lut"
],
"measure_method": "script",
"measure_device": "TP-Link HS110",
"measure_description": "Measured with script made by smonesi (steps: bri 50/sat 60/hue 2000) and linearly interpolated to provide more data"
}

View File

@@ -0,0 +1,10 @@
{
"name": "Hue Iris",
"standby_usage": 0.40,
"supported_modes": [
"lut"
],
"measure_method": "script",
"measure_device": "TP-Link HS110",
"measure_description": "Measured with script made by smonesi (steps: bri 50/sat 60/hue 2000) and linearly interpolated to provide more data"
}

View File

@@ -3,5 +3,8 @@
"standby_usage": 0.29,
"supported_modes": [
"lut"
]
}
],
"measure_method": "script",
"measure_device": "Shelly Plug S",
"measure_description": "Measured with script made by bramstroker and partially verified manual"
}

View File

@@ -1,10 +1,10 @@
{
"name": "Hue Go",
"standby_usage": 0.290,
"supported_modes": [
"linear"
"lut"
],
"linear_config": {
"min_power": 0,
"max_power": 6
}
}
"measure_method": "script",
"measure_device": "TP-Link HS110",
"measure_description": "Measured with script made by smonesi (steps: bri 20/sat 40/hue 2000/mired 30) and linearly interpolated to provide more data"
}

View File

@@ -0,0 +1,10 @@
{
"name":"Hue White and Color Ambiance LED Outdoor Lightstrip (5m)",
"standby_usage":0.4,
"supported_modes":[
"lut"
],
"measure_device":"Gosund SP111 on Tasmota 9.5.0",
"measure_description":"Measure Script for Tasmota",
"measure_method":"script"
}

View File

@@ -0,0 +1,10 @@
{
"name": "Hue White Ambiance E27",
"standby_usage": 0.29,
"supported_modes": [
"lut"
],
"measure_method": "script",
"measure_device": "TP-Link HS110",
"measure_description": "Measured with script made by smonesi (steps: bri 30/mired 30) and linearly interpolated to provide more data"
}

View File

@@ -2,5 +2,8 @@
"name": "Hue Being Ceiling Light",
"supported_modes": [
"lut"
]
],
"measure_method": "script",
"measure_device": "Shelly plug S",
"measure_description": "Used script utils/measure_shelly.py from repository"
}

View File

@@ -0,0 +1,10 @@
{
"name":"Hue Fair Ceiling Lamp",
"standby_usage":0.4,
"supported_modes":[
"lut"
],
"measure_device":"Gosund SP111 on Tasmota 9.5.0",
"measure_description":"Measure Script for Tasmota",
"measure_method":"script"
}

View File

@@ -0,0 +1,10 @@
{
"name":"Hue Fair Pendant",
"standby_usage":0.4,
"supported_modes":[
"lut"
],
"measure_device":"Gosund SP111 on Tasmota 9.5.0",
"measure_description":"Measure Script for Tasmota",
"measure_method":"script"
}

View File

@@ -2,5 +2,8 @@
"name": "Hue White Ambiance A19",
"supported_modes": [
"lut"
]
],
"measure_method": "script",
"measure_device": "Shelly plug S",
"measure_description": "Used script utils/measure_shelly.py from repository"
}

View File

@@ -3,5 +3,8 @@
"standby_usage": 0.39,
"supported_modes": [
"lut"
]
}
],
"measure_method": "script",
"measure_device": "Shelly Plug S",
"measure_description": "Measured with script made by bramstroker and partially verified manual"
}

View File

@@ -0,0 +1,10 @@
{
"name": " Hue White Ambiance Candle E14",
"supported_modes": [
"lut"
],
"standby_usage": 0.3,
"measure_method": "script",
"measure_device": "Shelly PM",
"measure_description": "Used script utils/measure_shelly.py from repository, standby_usage estimated"
}

View File

@@ -0,0 +1,10 @@
{
"name": "Hue White Ambiance Bulb A60 E27",
"supported_modes": [
"lut"
],
"measure_method": "script",
"measure_device": "Shelly PM",
"standby_usage": 0.4,
"measure_description": "Used script utils/measure_shelly.py from repository, estimated standby"
}

View File

@@ -2,5 +2,8 @@
"name": "Hue White Filament Bulb A60 E27",
"supported_modes": [
"lut"
]
],
"measure_method": "script",
"measure_device": "Shelly plug S",
"measure_description": "Used script utils/measure_shelly.py from repository"
}

View File

@@ -0,0 +1,10 @@
{
"name": "Hue White 1600 A67 E27 1600lm",
"standby_usage": 0.41,
"supported_modes": [
"lut"
],
"measure_method": "script",
"measure_device": "Shelly Plug S",
"measure_description": "Measured twice with script made by bramstroker using 9 sec pauses and partially verified manual"
}

View File

@@ -3,5 +3,8 @@
"standby_usage": 0.29,
"supported_modes": [
"lut"
]
],
"measure_method": "manual",
"measure_device": "Power Recorder; PR10-D (Model); Shenzen Zhurui Technology (Manufacturer)",
"measure_description": "Bulbs was measured twice at each dim setting (100 points) and mean value was used when there were differences"
}

View File

@@ -0,0 +1,10 @@
{
"name": "Hue White A60 E27 800lm",
"standby_usage": 0.4,
"supported_modes": [
"lut"
],
"measure_method": "manual",
"measure_device": "RRPM02 (Model); 4897037690801 (Barcode); Reduction Revolution Plug-in Power Meter",
"measure_description": "Was not able to measure the standby usage, the value is assumed. Two bulbs were measured and the higher reading at each dim setting (0-100) was used"
}

View File

@@ -3,5 +3,8 @@
"standby_usage": 0.39,
"supported_modes": [
"lut"
]
],
"measure_method": "manual",
"measure_device": "Power Recorder; PR10-D (Model); Shenzen Zhurui Technology (Manufacturer)",
"measure_description": "Bulbs was measured twice at each dim setting (100 points) and mean value was used when there were differences"
}

View File

@@ -0,0 +1,10 @@
{
"measure_device": "Shelly Plug S",
"measure_method": "script",
"measure_description": "Measured with utils/measure_v2, SAMPLE_COUNT=3",
"name": "Hue White Filament Bulb G93 E27 w/ BT",
"standby_usage": 0.35,
"supported_modes": [
"lut"
]
}

View File

@@ -0,0 +1,10 @@
{
"name":"Hue White Filament Bulb ST64 E27",
"standby_usage":0.5,
"supported_modes":[
"lut"
],
"measure_device":"Gosund SP111 on Tasmota 9.5.0",
"measure_description":"Measure Script for Tasmota",
"measure_method":"script"
}

View File

@@ -0,0 +1,10 @@
{
"measure_device": "Sonoff DualR3",
"measure_description": "Took several readings on every 10% of the lamp and did a linear regression to estimate the correct consumptions. Standby usage is assumed",
"measure_method": "script",
"name": "Sonoff B02 Light with dimmer and temperature",
"standby_usage": 0.4,
"supported_modes": [
"lut"
]
}

View File

@@ -0,0 +1,10 @@
{
"name": "Yeelight LED 600lm 4000K 8W WiFi",
"standby_usage": 1.1,
"supported_modes": [
"lut"
],
"measure_method": "manual",
"measure_device": "RRPM02 (Model); 4897037690801 (Barcode); Reduction Revolution Plug-in Power Meter",
"measure_description": "No comments"
}

View File

@@ -1,4 +1,8 @@
{
"after_dependencies": [
"integration",
"utility_meter"
],
"codeowners": [
"@bramstroker"
],
@@ -7,7 +11,8 @@
"fan",
"switch",
"binary_sensor",
"light"
"light",
"template"
],
"documentation": "https://github.com/bramstroker/homeassistant-powercalc",
"domain": "powercalc",
@@ -15,5 +20,5 @@
"issue_tracker": "https://github.com/bramstroker/homeassistant-powercalc/issues",
"name": "Power consumption",
"requirements": [],
"version": "v0.2.7"
"version": "v0.6.0"
}

View File

@@ -0,0 +1,86 @@
"""Utilities for auto discovery of light models."""
from __future__ import annotations
import logging
import os
from collections import namedtuple
from typing import NamedTuple, Optional
import homeassistant.helpers.entity_registry as er
from homeassistant.components.hue.const import DOMAIN as HUE_DOMAIN
from homeassistant.components.light import Light
from homeassistant.helpers.typing import HomeAssistantType
from .const import CONF_CUSTOM_MODEL_DIRECTORY, CONF_MANUFACTURER, CONF_MODEL
from .light_model import LightModel
_LOGGER = logging.getLogger(__name__)
async def get_light_model(
hass: HomeAssistantType, entity_entry, config: dict
) -> Optional[LightModel]:
manufacturer = config.get(CONF_MANUFACTURER)
model = config.get(CONF_MODEL)
if (manufacturer is None or model is None) and entity_entry:
hue_model_info = await autodiscover_hue_model(hass, entity_entry)
if hue_model_info:
manufacturer = hue_model_info.manufacturer
model = hue_model_info.model
if manufacturer is None or model is None:
return None
custom_model_directory = config.get(CONF_CUSTOM_MODEL_DIRECTORY)
if custom_model_directory:
custom_model_directory = os.path.join(
hass.config.config_dir, custom_model_directory
)
return LightModel(manufacturer, model, custom_model_directory)
async def autodiscover_hue_model(
hass: HomeAssistantType, entity_entry
) -> Optional[HueModelInfo]:
# When Philips Hue model is enabled we can auto discover manufacturer and model from the bridge data
if hass.data.get(HUE_DOMAIN) is None or entity_entry.platform != "hue":
return
light = await find_hue_light(hass, entity_entry)
if light is None:
_LOGGER.error(
"Cannot autodiscover model for '%s', not found in the hue bridge api",
entity_entry.entity_id,
)
return
_LOGGER.debug(
"Auto discovered Hue model for entity %s: (manufacturer=%s, model=%s)",
entity_entry.entity_id,
light.manufacturername,
light.modelid,
)
return HueModelInfo(light.manufacturername, light.modelid)
async def find_hue_light(
hass: HomeAssistantType, entity_entry: er.RegistryEntry
) -> Light | None:
"""Find the light in the Hue bridge, we need to extract the model id."""
bridge = hass.data[HUE_DOMAIN][entity_entry.config_entry_id]
lights = bridge.api.lights
for light_id in lights:
light = bridge.api.lights[light_id]
if light.uniqueid == entity_entry.unique_id:
return light
return None
class HueModelInfo(NamedTuple):
manufacturer: str
model: str

View File

@@ -2,73 +2,96 @@
from __future__ import annotations
from datetime import timedelta
import logging
import os
from typing import Optional
import homeassistant.helpers.config_validation as cv
import homeassistant.helpers.entity_registry as er
import voluptuous as vol
from homeassistant.components import (
binary_sensor,
climate,
device_tracker,
fan,
input_boolean,
input_select,
light,
media_player,
remote,
sensor,
switch,
vacuum,
)
from homeassistant.components.hue.const import DOMAIN as HUE_DOMAIN
from homeassistant.components.light import PLATFORM_SCHEMA, Light
from homeassistant.components.integration.sensor import (
TRAPEZOIDAL_METHOD,
IntegrationSensor,
)
from homeassistant.components.light import PLATFORM_SCHEMA
from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT
from homeassistant.components.utility_meter import DEFAULT_OFFSET
from homeassistant.components.utility_meter.sensor import UtilityMeterSensor
from homeassistant.const import (
CONF_ENTITY_ID,
CONF_NAME,
CONF_SCAN_INTERVAL,
DEVICE_CLASS_POWER,
EVENT_HOMEASSISTANT_START,
POWER_WATT,
STATE_NOT_HOME,
STATE_OFF,
STATE_STANDBY,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
TIME_HOURS,
)
from homeassistant.core import callback, split_entity_id
from homeassistant.helpers.entity import Entity, async_generate_entity_id
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import (
async_track_state_change_event,
async_track_time_interval,
)
from homeassistant.helpers.typing import (
ConfigType,
DiscoveryInfoType,
HomeAssistantType,
)
from homeassistant.core import split_entity_id
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.helpers.typing import HomeAssistantType
from .common import SourceEntity
from .const import (
CALCULATION_MODES,
CONF_CREATE_ENERGY_SENSOR,
CONF_CREATE_ENERGY_SENSORS,
CONF_CREATE_UTILITY_METERS,
CONF_CUSTOM_MODEL_DIRECTORY,
CONF_DISABLE_STANDBY_USAGE,
CONF_ENERGY_SENSOR_NAMING,
CONF_FIXED,
CONF_LINEAR,
CONF_MANUFACTURER,
CONF_MAX_WATT,
CONF_MIN_WATT,
CONF_MODE,
CONF_MODEL,
CONF_MULTIPLY_FACTOR,
CONF_MULTIPLY_FACTOR_STANDBY,
CONF_POWER_SENSOR_NAMING,
CONF_STANDBY_USAGE,
CONF_WATT,
CONF_UTILITY_METER_TYPES,
DATA_CALCULATOR_FACTORY,
DOMAIN,
DOMAIN_CONFIG,
MODE_FIXED,
MODE_LINEAR,
MODE_LUT,
)
from .errors import ModelNotSupported, StrategyConfigurationError, UnsupportedMode
from .light_model import LightModel
from .model_discovery import get_light_model
from .strategy_fixed import CONFIG_SCHEMA as FIXED_SCHEMA
from .strategy_interface import PowerCalculationStrategyInterface
from .strategy_linear import CONFIG_SCHEMA as LINEAR_SCHEMA
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(minutes=30)
PLATFORM_SCHEMA = vol.All(
cv.deprecated(CONF_MIN_WATT),
cv.deprecated(CONF_MAX_WATT),
cv.deprecated(CONF_WATT),
PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_NAME): cv.string,
@@ -78,106 +101,223 @@ PLATFORM_SCHEMA = vol.All(
switch.DOMAIN,
fan.DOMAIN,
binary_sensor.DOMAIN,
climate.DOMAIN,
device_tracker.DOMAIN,
remote.DOMAIN,
media_player.DOMAIN,
input_boolean.DOMAIN,
input_select.DOMAIN,
sensor.DOMAIN,
vacuum.DOMAIN,
)
),
vol.Optional(CONF_MODEL): cv.string,
vol.Optional(CONF_MANUFACTURER): cv.string,
vol.Optional(CONF_MODE): vol.In([MODE_LUT, MODE_FIXED, MODE_LINEAR]),
vol.Optional(CONF_MIN_WATT): cv.string,
vol.Optional(CONF_MAX_WATT): cv.string,
vol.Optional(CONF_WATT): cv.string,
vol.Optional(CONF_MODE): vol.In(CALCULATION_MODES),
vol.Optional(CONF_STANDBY_USAGE): vol.Coerce(float),
vol.Optional(CONF_DISABLE_STANDBY_USAGE, default=False): cv.boolean,
vol.Optional(CONF_CUSTOM_MODEL_DIRECTORY): cv.string,
vol.Optional(CONF_FIXED): FIXED_SCHEMA,
vol.Optional(CONF_LINEAR): LINEAR_SCHEMA,
vol.Optional(CONF_CREATE_ENERGY_SENSOR): cv.boolean,
vol.Optional(CONF_MULTIPLY_FACTOR): vol.Coerce(float),
vol.Optional(CONF_MULTIPLY_FACTOR_STANDBY, default=False): cv.boolean,
}
),
)
NAME_FORMAT = "{} power"
ENERGY_ICON = "mdi:lightning-bolt"
ATTR_SOURCE_ENTITY = "source_entity"
ATTR_SOURCE_DOMAIN = "source_domain"
OFF_STATES = [STATE_OFF, STATE_NOT_HOME, STATE_STANDBY]
async def async_setup_platform(
hass: HomeAssistantType, config, async_add_entities, discovery_info=None
hass: HomeAssistantType,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
):
"""Set up the sensor platform."""
calculation_strategy_factory = hass.data[DOMAIN][DATA_CALCULATOR_FACTORY]
component_config = hass.data[DOMAIN][DOMAIN_CONFIG]
entity_id = config[CONF_ENTITY_ID]
source_entity = config[CONF_ENTITY_ID]
source_entity_domain, source_object_id = split_entity_id(source_entity)
entity_registry = await er.async_get_registry(hass)
entity_entry = entity_registry.async_get(entity_id)
entity_state = hass.states.get(entity_id)
entity_entry = entity_registry.async_get(source_entity)
unique_id = None
if entity_entry:
entity_name = entity_entry.name or entity_entry.original_name
entity_domain = entity_entry.domain
source_entity_name = entity_entry.name or entity_entry.original_name
source_entity_domain = entity_entry.domain
unique_id = entity_entry.unique_id
elif entity_state:
entity_name = entity_state.name
entity_domain = entity_state.domain
else:
entity_name = split_entity_id(entity_id)[1].replace("_", " ")
entity_domain = split_entity_id(entity_id)[0]
source_entity_name = source_object_id.replace("_", " ")
name = config.get(CONF_NAME) or NAME_FORMAT.format(entity_name)
entity_state = hass.states.get(source_entity)
if entity_state:
source_entity_name = entity_state.name
capabilities = entity_entry.capabilities if entity_entry else []
source_entity = SourceEntity(
unique_id,
source_object_id,
source_entity,
source_entity_name,
source_entity_domain,
capabilities,
)
try:
power_sensor = await create_power_sensor(
hass, entity_entry, config, component_config, source_entity
)
except (ModelNotSupported, StrategyConfigurationError) as err:
pass
entities_to_add = [power_sensor]
should_create_energy_sensor = component_config.get(CONF_CREATE_ENERGY_SENSORS)
if CONF_CREATE_ENERGY_SENSOR in config:
should_create_energy_sensor = config.get(CONF_CREATE_ENERGY_SENSOR)
if should_create_energy_sensor:
energy_sensor = await create_energy_sensor(
hass, component_config, config, power_sensor, source_entity
)
entities_to_add.append(energy_sensor)
if component_config.get(CONF_CREATE_UTILITY_METERS):
meter_types = component_config.get(CONF_UTILITY_METER_TYPES)
for meter_type in meter_types:
entities_to_add.append(
create_utility_meter_sensor(energy_sensor, meter_type)
)
async_add_entities(entities_to_add)
async def create_power_sensor(
hass: HomeAssistantType,
entity_entry,
sensor_config: dict,
component_config: dict,
source_entity: SourceEntity,
) -> VirtualPowerSensor:
"""Create the power sensor entity"""
calculation_strategy_factory = hass.data[DOMAIN][DATA_CALCULATOR_FACTORY]
name_pattern = component_config.get(CONF_POWER_SENSOR_NAMING)
name = sensor_config.get(CONF_NAME) or source_entity.name
name = name_pattern.format(name)
object_id = sensor_config.get(CONF_NAME) or source_entity.object_id
entity_id = async_generate_entity_id("sensor.{}", name_pattern.format(object_id), hass=hass)
light_model = None
try:
light_model = await get_light_model(hass, entity_entry, config)
except (ModelNotSupported) as err:
_LOGGER.info("Model not found in library %s: %s", entity_id, err)
light_model = await get_light_model(hass, entity_entry, sensor_config)
except ModelNotSupported as err:
mode = select_calculation_mode(sensor_config, None)
if mode == MODE_LUT:
_LOGGER.error(
"Model not found in library %s: %s", source_entity.entity_id, err
)
raise err
try:
mode = select_calculation_mode(config, light_model)
mode = select_calculation_mode(sensor_config, light_model)
calculation_strategy = calculation_strategy_factory.create(
config, mode, light_model, entity_domain
sensor_config, mode, light_model, source_entity.domain
)
await calculation_strategy.validate_config(entity_entry)
await calculation_strategy.validate_config(source_entity)
except (ModelNotSupported, UnsupportedMode) as err:
_LOGGER.error("Skipping sensor setup %s: %s", entity_id, err)
return
_LOGGER.error("Skipping sensor setup %s: %s", source_entity.entity_id, err)
raise err
except StrategyConfigurationError as err:
_LOGGER.error("Error setting up calculation strategy: %s", err)
return
_LOGGER.error(
"Error setting up calculation strategy for %s: %s",
source_entity.entity_id,
err,
)
raise err
standby_usage = None
if config.get(CONF_DISABLE_STANDBY_USAGE) == False:
standby_usage = config.get(CONF_STANDBY_USAGE)
if not sensor_config.get(CONF_DISABLE_STANDBY_USAGE):
standby_usage = sensor_config.get(CONF_STANDBY_USAGE)
if standby_usage is None and light_model is not None:
standby_usage = light_model.standby_usage
_LOGGER.debug(
"Setting up power sensor. entity_id:%s sensor_name:%s strategy=%s manufacturer=%s model=%s standby_usage=%s",
entity_id,
"Setting up power sensor. entity_id:%s sensor_name:%s strategy=%s manufacturer=%s model=%s standby_usage=%s unique_id=%s",
source_entity.entity_id,
name,
calculation_strategy.__class__.__name__,
light_model.manufacturer if light_model else "",
light_model.model if light_model else "",
standby_usage,
source_entity.unique_id,
)
async_add_entities(
[
VirtualPowerSensor(
power_calculator=calculation_strategy,
name=name,
entity_id=entity_id,
unique_id=unique_id,
standby_usage=standby_usage,
)
]
return VirtualPowerSensor(
power_calculator=calculation_strategy,
entity_id=entity_id,
name=name,
source_entity=source_entity.entity_id,
source_domain=source_entity.domain,
unique_id=source_entity.unique_id,
standby_usage=standby_usage,
scan_interval=component_config.get(CONF_SCAN_INTERVAL),
multiply_factor=sensor_config.get(CONF_MULTIPLY_FACTOR),
multiply_factor_standby=sensor_config.get(CONF_MULTIPLY_FACTOR_STANDBY),
)
def select_calculation_mode(config: dict, light_model: LightModel):
async def create_energy_sensor(
hass: HomeAssistantType,
component_config: dict,
sensor_config: dict,
power_sensor: VirtualPowerSensor,
source_entity: SourceEntity,
) -> VirtualEnergySensor:
name_pattern = component_config.get(CONF_ENERGY_SENSOR_NAMING)
name = sensor_config.get(CONF_NAME) or source_entity.name
name = name_pattern.format(name)
object_id = sensor_config.get(CONF_NAME) or source_entity.object_id
entity_id = async_generate_entity_id(
"sensor.{}", name_pattern.format(object_id), hass=hass
)
_LOGGER.debug("Creating energy sensor: %s", name)
return VirtualEnergySensor(
source_entity=power_sensor.entity_id,
unique_id=source_entity.unique_id,
entity_id=entity_id,
name=name,
round_digits=4,
unit_prefix="k",
unit_of_measurement=None,
unit_time=TIME_HOURS,
integration_method=TRAPEZOIDAL_METHOD,
powercalc_source_entity=source_entity.entity_id,
powercalc_source_domain=source_entity.domain,
)
def create_utility_meter_sensor(
energy_sensor: VirtualEnergySensor, meter_type: str
) -> VirtualUtilityMeterSensor:
name = f"{energy_sensor.name} {meter_type}"
entity_id = f"{energy_sensor.entity_id}_{meter_type}"
_LOGGER.debug("Creating utility_meter sensor: %s", name)
return VirtualUtilityMeterSensor(
energy_sensor.entity_id, name, meter_type, entity_id
)
def select_calculation_mode(config: dict, light_model: LightModel) -> str:
"""Select the calculation mode"""
config_mode = config.get(CONF_MODE)
if config_mode:
@@ -192,78 +332,11 @@ def select_calculation_mode(config: dict, light_model: LightModel):
if light_model:
return light_model.supported_modes[0]
# BC compat
if config.get(CONF_MIN_WATT):
return MODE_LINEAR
# BC compat
if config.get(CONF_WATT):
return MODE_FIXED
raise UnsupportedMode(
"Cannot select a mode (LINEAR, FIXED or LUT), supply it in the config"
)
async def get_light_model(hass, entity_entry, config: dict) -> Optional[LightModel]:
manufacturer = config.get(CONF_MANUFACTURER)
model = config.get(CONF_MODEL)
if (manufacturer is None or model is None) and entity_entry:
hue_model_data = await autodiscover_hue_model(hass, entity_entry)
if hue_model_data:
manufacturer = hue_model_data["manufacturer"]
model = hue_model_data["model"]
if manufacturer is None or model is None:
return None
custom_model_directory = config.get(CONF_CUSTOM_MODEL_DIRECTORY)
if custom_model_directory:
custom_model_directory = os.path.join(
hass.config.config_dir, custom_model_directory
)
return LightModel(manufacturer, model, custom_model_directory)
async def autodiscover_hue_model(hass, entity_entry):
# When Philips Hue model is enabled we can auto discover manufacturer and model from the bridge data
if hass.data.get(HUE_DOMAIN) == None or entity_entry.platform != "hue":
return
light = await find_hue_light(hass, entity_entry)
if light is None:
_LOGGER.error(
"Cannot autodisover model for '%s', not found in the hue bridge api",
entity_entry.entity_id,
)
return
_LOGGER.debug(
"Auto discovered Hue model for entity %s: (manufacturer=%s, model=%s)",
entity_entry.entity_id,
light.manufacturername,
light.modelid,
)
return {"manufacturer": light.manufacturername, "model": light.modelid}
async def find_hue_light(
hass: HomeAssistantType, entity_entry: er.RegistryEntry
) -> Light | None:
"""Find the light in the Hue bridge, we need to extract the model id."""
bridge = hass.data[HUE_DOMAIN][entity_entry.config_entry_id]
lights = bridge.api.lights
for light_id in lights:
light = bridge.api.lights[light_id]
if light.uniqueid == entity_entry.unique_id:
return light
return None
class VirtualPowerSensor(Entity):
"""Representation of a Sensor."""
@@ -274,19 +347,29 @@ class VirtualPowerSensor(Entity):
def __init__(
self,
power_calculator: PowerCalculationStrategyInterface,
name: str,
entity_id: str,
name: str,
source_entity: str,
source_domain: str,
unique_id: str,
standby_usage: float | None,
scan_interval,
multiply_factor: float | None,
multiply_factor_standby: bool,
):
"""Initialize the sensor."""
self._power_calculator = power_calculator
self._entity_id = entity_id
self._source_entity = source_entity
self._source_domain = source_domain
self._name = name
self._power = None
self._unique_id = unique_id
self._standby_usage = standby_usage
self._attr_force_update = True
self._attr_unique_id = unique_id
self._scan_interval = scan_interval
self._multiply_factor = multiply_factor
self._multiply_factor_standby = multiply_factor_standby
self.entity_id = entity_id
async def async_added_to_hass(self):
"""Register callbacks."""
@@ -301,29 +384,49 @@ class VirtualPowerSensor(Entity):
"""Add listeners and get initial state."""
async_track_state_change_event(
self.hass, [self._entity_id], appliance_state_listener
self.hass, [self._source_entity], appliance_state_listener
)
new_state = self.hass.states.get(self._entity_id)
new_state = self.hass.states.get(self._source_entity)
await self._update_power_sensor(new_state)
@callback
def async_update(event_time=None):
"""Update the entity."""
self.async_schedule_update_ha_state(True)
async_track_time_interval(self.hass, async_update, self._scan_interval)
self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START, home_assistant_startup
)
async def _update_power_sensor(self, state) -> bool:
"""Update power sensor based on new dependant hue light state."""
if state is None or state.state == STATE_UNKNOWN:
if (
state is None
or state.state == STATE_UNKNOWN
or state.state == STATE_UNAVAILABLE
):
self._power = None
self.async_write_ha_state()
return False
if state.state == STATE_UNAVAILABLE:
return False
if state.state == STATE_OFF or state.state == STATE_STANDBY:
if state.state in OFF_STATES:
self._power = self._standby_usage or 0
if self._multiply_factor and self._multiply_factor_standby:
self._power *= self._multiply_factor
else:
self._power = await self._power_calculator.calculate(state)
if self._multiply_factor and self._power is not None:
self._power *= self._multiply_factor
if self._power is None:
self.async_write_ha_state()
return False
self._power = round(self._power, 2)
_LOGGER.debug(
'State changed to "%s" for entity "%s". Power:%s',
@@ -335,6 +438,14 @@ class VirtualPowerSensor(Entity):
self.async_write_ha_state()
return True
@property
def extra_state_attributes(self):
"""Return entity state attributes."""
return {
ATTR_SOURCE_ENTITY: self._source_entity,
ATTR_SOURCE_DOMAIN: self._source_domain,
}
@property
def name(self):
"""Return the name of the sensor."""
@@ -345,12 +456,56 @@ class VirtualPowerSensor(Entity):
"""Return the state of the sensor."""
return self._power
@property
def unique_id(self):
"""Return a unique id."""
return self._unique_id
@property
def available(self):
"""Return True if entity is available."""
return self._power is not None
class VirtualEnergySensor(IntegrationSensor):
def __init__(
self,
source_entity,
unique_id,
entity_id,
name,
round_digits,
unit_prefix,
unit_time,
unit_of_measurement,
integration_method,
powercalc_source_entity: str,
powercalc_source_domain: str,
):
super().__init__(
source_entity,
name,
round_digits,
unit_prefix,
unit_time,
unit_of_measurement,
integration_method,
)
self._powercalc_source_entity = powercalc_source_entity
self._powercalc_source_domain = powercalc_source_domain
self.entity_id = entity_id
if unique_id:
self._attr_unique_id = f"{unique_id}_energy"
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the acceleration sensor."""
state_attr = super().extra_state_attributes
state_attr[ATTR_SOURCE_ENTITY] = self._powercalc_source_entity
state_attr[ATTR_SOURCE_DOMAIN] = self._powercalc_source_domain
return state_attr
@property
def icon(self):
return ENERGY_ICON
class VirtualUtilityMeterSensor(UtilityMeterSensor):
def __init__(self, source_entity, name, meter_type, entity_id):
super().__init__(source_entity, name, meter_type, DEFAULT_OFFSET, False)
self.entity_id = entity_id

View File

@@ -3,22 +3,27 @@ from __future__ import annotations
from typing import Optional
import homeassistant.helpers.config_validation as cv
import homeassistant.helpers.entity_registry as er
import voluptuous as vol
from homeassistant.components import climate, media_player, vacuum
from homeassistant.core import State
from .common import SourceEntity
from .const import CONF_POWER, CONF_STATES_POWER
from .errors import StrategyConfigurationError
from .strategy_interface import PowerCalculationStrategyInterface
CONFIG_SCHEMA = vol.Schema(
{
vol.Optional(CONF_POWER): vol.Coerce(float),
vol.Optional(CONF_STATES_POWER, default={}): vol.Schema(
{cv.string: vol.Coerce(float)}
),
vol.Optional(CONF_STATES_POWER): vol.Schema({cv.string: vol.Coerce(float)}),
}
)
STATE_BASED_ENTITY_DOMAINS = [
climate.DOMAIN,
vacuum.DOMAIN,
]
class FixedStrategy(PowerCalculationStrategyInterface):
def __init__(
@@ -28,11 +33,27 @@ class FixedStrategy(PowerCalculationStrategyInterface):
self._per_state_power = per_state_power
async def calculate(self, entity_state: State) -> Optional[int]:
if entity_state.state in self._per_state_power:
return self._per_state_power.get(entity_state.state)
if self._per_state_power is not None:
# Lookup by state
if entity_state.state in self._per_state_power:
return self._per_state_power.get(entity_state.state)
else:
# Lookup by state attribute (attribute|value)
for state_key, power in self._per_state_power.items():
if "|" in state_key:
attribute, value = state_key.split("|", 2)
if entity_state.attributes.get(attribute) == value:
return power
return self._power
async def validate_config(self, entity_entry: er.RegistryEntry):
async def validate_config(self, source_entity: SourceEntity):
"""Validate correct setup of the strategy"""
pass
if (
source_entity.domain in STATE_BASED_ENTITY_DOMAINS
and self._per_state_power is None
):
raise StrategyConfigurationError(
"This entity can only work with 'state_power' not 'power'"
)

View File

@@ -1,14 +1,15 @@
from typing import Optional
import homeassistant.helpers.entity_registry as er
from homeassistant.core import State
from .common import SourceEntity
class PowerCalculationStrategyInterface:
async def calculate(self, entity_state: State) -> Optional[int]:
"""Calculate power consumption based on entity state"""
pass
async def validate_config(self, entity_entry: er.RegistryEntry):
async def validate_config(self, source_entity: SourceEntity):
"""Validate correct setup of the strategy"""
pass

View File

@@ -4,7 +4,6 @@ import logging
from typing import Optional
import homeassistant.helpers.config_validation as cv
import homeassistant.helpers.entity_registry as er
import voluptuous as vol
from homeassistant.components import fan, light
from homeassistant.components.fan import ATTR_PERCENTAGE
@@ -12,10 +11,12 @@ from homeassistant.components.light import ATTR_BRIGHTNESS
from homeassistant.core import State
from homeassistant.helpers.config_validation import entity_domain
from .common import SourceEntity
from .const import CONF_CALIBRATE, CONF_MAX_POWER, CONF_MIN_POWER
from .errors import StrategyConfigurationError
from .strategy_interface import PowerCalculationStrategyInterface
ALLOWED_DOMAINS = [fan.DOMAIN, light.DOMAIN]
CONFIG_SCHEMA = vol.Schema(
{
vol.Optional(CONF_CALIBRATE): vol.All(
@@ -40,6 +41,9 @@ class LinearStrategy(PowerCalculationStrategyInterface):
if entity_state.domain == light.DOMAIN:
value = attrs.get(ATTR_BRIGHTNESS)
# Some integrations set a higher brightness value than 255, causing powercalc to misbehave
if value > 255:
value = 255
if value is None:
_LOGGER.error("No brightness for entity: %s", entity_state.entity_id)
return None
@@ -92,9 +96,16 @@ class LinearStrategy(PowerCalculationStrategyInterface):
sorted_list = sorted(list, key=lambda tup: tup[0])
return sorted_list
async def validate_config(self, entity_entry: er.RegistryEntry):
async def validate_config(self, source_entity: SourceEntity):
"""Validate correct setup of the strategy"""
if source_entity.domain not in ALLOWED_DOMAINS:
raise StrategyConfigurationError(
"Entity not supported for linear mode. Must be one of: {}".format(
",".join(ALLOWED_DOMAINS)
)
)
if self._config.get(CONF_CALIBRATE) is None:
if self._config.get(CONF_MIN_POWER) is None:
raise StrategyConfigurationError("You must supply min power")

View File

@@ -8,7 +8,6 @@ from csv import reader
from functools import partial
from typing import Optional
import homeassistant.helpers.entity_registry as er
from homeassistant.components import light
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
@@ -22,6 +21,7 @@ from homeassistant.components.light import (
)
from homeassistant.core import State
from .common import SourceEntity
from .errors import (
LutFileNotFound,
ModelNotSupported,
@@ -45,7 +45,7 @@ class LutRegistry:
) -> dict | None:
cache_key = f"{light_model.manufacturer}_{light_model.model}_{color_mode}"
lookup_dict = self._lookup_dictionaries.get(cache_key)
if lookup_dict == None:
if lookup_dict is None:
defaultdict_of_dict = partial(defaultdict, dict)
lookup_dict = defaultdict(defaultdict_of_dict)
@@ -101,6 +101,8 @@ class LutStrategy(PowerCalculationStrategyInterface):
if brightness is None:
_LOGGER.error("No brightness for entity: %s", entity_state.entity_id)
return None
if brightness > 255:
brightness = 255
try:
lookup_table = await self._lut_registry.get_lookup_dictionary(
@@ -141,23 +143,26 @@ class LutStrategy(PowerCalculationStrategyInterface):
or dict[min(dict.keys(), key=lambda key: abs(key - search_key))]
)
async def validate_config(
self,
entity_entry: er.RegistryEntry,
):
if entity_entry.domain != light.DOMAIN:
def get_nearest_lower(self, dict: dict, search_key):
return (
dict.get(search_key)
or dict[min(dict.keys(), key=lambda key: abs(key - search_key))]
)
async def validate_config(self, source_entity: SourceEntity):
if source_entity.domain != light.DOMAIN:
raise StrategyConfigurationError("Only light entities can use the LUT mode")
if self._model.manufacturer is None:
_LOGGER.error(
"Manufacturer not supplied for entity: %s", entity_entry.entity_id
"Manufacturer not supplied for entity: %s", source_entity.entity_id
)
if self._model.model is None:
_LOGGER.error("Model not supplied for entity: %s", entity_entry.entity_id)
_LOGGER.error("Model not supplied for entity: %s", source_entity.entity_id)
return
supported_color_modes = entity_entry.capabilities[
supported_color_modes = source_entity.capabilities[
light.ATTR_SUPPORTED_COLOR_MODES
]
for color_mode in supported_color_modes:

View File

@@ -22,20 +22,11 @@ sensor:
# Custom Integration Powercalc
## Power consumption lights
- platform: powercalc
name: power_light_bedroom_ceiling_1
entity_id: light.bedroom_ceiling_1
manufacturer: signify
model: LCT010
- platform: powercalc
name: power_light_bedroom_ceiling_2
entity_id: light.bedroom_ceiling_2
manufacturer: signify
model: LCT010
- platform: powercalc
name: power_light_bedroom_ceiling_3
entity_id: light.bedroom_ceiling_3
name: power_light_bedroom_ceiling
entity_id: light.bedroom_ceiling
manufacturer: signify
model: LCT010
multiply_factor: 3
- platform: powercalc
name: power_light_bedroom_bed
entity_id: light.bedroom_bed

View File

@@ -15,20 +15,11 @@ sensor:
# Custom Integration Powercalc
## Power consumption lights
- platform: powercalc
name: power_light_dressroom_ceiling_1
entity_id: light.dressroom_ceiling_1
manufacturer: signify
model: LCT010
- platform: powercalc
name: power_light_dressroom_ceiling_2
entity_id: light.dressroom_ceiling_2
manufacturer: signify
model: LCT010
- platform: powercalc
name: power_light_dressroom_ceiling_3
entity_id: light.dressroom_ceiling_3
name: power_light_dressroom
entity_id: light.dressroom
manufacturer: signify
model: LCT010
multiply_factor: 3
automation:
# Turn lights on (if not already on) when motion is detected

View File

@@ -14,6 +14,10 @@ input_number:
max: 2
step: 0.01
# Powercalc General Settings
powercalc:
create_energy_sensors: false
sensor:
# Convert power (W) to energy (kWh)
- platform: integration
@@ -29,10 +33,10 @@ template:
device_class: power
unit_of_measurement: W
state: >
{% set office = states('sensor.power_light_office_ceiling_1')|float + states('sensor.power_light_office_ceiling_2')|float + states('sensor.power_light_office_ceiling_3')|float %}
{% set bedroom_ceiling = states('sensor.power_light_bedroom_ceiling_1')|float + states('sensor.power_light_bedroom_ceiling_2')|float + states('sensor.power_light_bedroom_ceiling_3')|float %}
{% set office = states('sensor.power_light_office')|float %}
{% set bedroom_ceiling = states('sensor.power_light_bedroom_ceiling')|float %}
{% set bedroom_bed = states('sensor.power_light_bedroom_bed')|float %}
{% set dressroom = states('sensor.power_light_dressroom_ceiling_1')|float + states('sensor.power_light_dressroom_ceiling_2')|float + states('sensor.power_light_dressroom_ceiling_3')|float %}
{% set dressroom = states('sensor.power_light_dressroom')|float %}
{% set livingroom = states('sensor.power_light_livingroom_floor_front')|float + states('sensor.power_light_livingroom_back')|float %}
{{ office + bedroom_ceiling + bedroom_bed + dressroom + livingroom }}
# Current Electricity Tariff

View File

@@ -52,10 +52,9 @@ sensor:
- platform: powercalc
name: power_receiver_livingroom
entity_id: media_player.receiver_livingroom
standby_usage: 2.7
fixed:
states_power:
on: 500
off: 2.7
power: 500
# Convert power (W) to energy (kWh) for beamer
- platform: integration
source: sensor.power_beamer

View File

@@ -32,26 +32,13 @@ sensor:
# Custom Integration Powercalc
## Power consumption lights
- platform: powercalc
name: power_light_office_ceiling_1
entity_id: light.office_ceiling_1
name: power_light_office
entity_id: light.office
multiply_factor: 3
linear:
min_power: 0.5
max_power: 5
standby_usage: 0.2
- platform: powercalc
name: power_light_office_ceiling_2
entity_id: light.office_ceiling_2
linear:
min_power: 0.5
max_power: 5
standby_usage: 0.2
- platform: powercalc
name: power_light_office_ceiling_3
entity_id: light.office_ceiling_3
linear:
min_power: 0.5
max_power: 5
standby_usage: 0.2
standby_usage: 0.6
automation:
# Turn lights on (if not already on) when motion is detected and lux below threshold