Skip to content

Commit

Permalink
Add support for smart speakers and add some profiles to the library (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
CloCkWeRX committed Oct 5, 2022
1 parent 8215143 commit cf9d046
Show file tree
Hide file tree
Showing 21 changed files with 624 additions and 103 deletions.
114 changes: 71 additions & 43 deletions .github/scripts/supported_models/generate_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,58 +16,86 @@
from aliases import MANUFACTURER_DIRECTORY_MAPPING
from pytablewriter import MarkdownTableWriter

DEVICE_TYPES = [
("light", "Lights"),
("smart_speaker", "Smart speakers"),
("smart_switch", "Smart switches")
]

PROJECT_ROOT = os.path.realpath(
os.path.join(os.path.abspath(__file__), "../../../../")
)

def generate_supported_model_list():
writer = MarkdownTableWriter()
writer.header_list = [
"manufacturer",
"model id",
"name",
"calculation modes",
"color modes",
"aliases",
]

def generate_supported_model_list():
"""Generate static file containing the supported models."""
project_root = os.path.realpath(
os.path.join(os.path.abspath(__file__), "../../../../")
)
data_dir = f"{project_root}/custom_components/powercalc/data"
with open(os.path.join(project_root, "docs/supported_models.md"), "w") as md_file:
models = get_model_list()

output = ""
for device_type in DEVICE_TYPES:
writer = MarkdownTableWriter()
headers = [
"manufacturer",
"model id",
"name",
"calculation modes",
"aliases",
]
if device_type == "light":
headers.append("color modes")

writer.header_list = headers

relevant_models = [model for model in models if (model.get("device_type") or "light") == device_type[0]]
rows = []
for json_path in glob.glob(
f"{data_dir}/*/*/model.json",
recursive=True,
):
with open(json_path) as json_file:
model_directory = os.path.dirname(json_path)
model_data: dict = json.load(json_file)
model = os.path.basename(model_directory)
manufacturer = os.path.basename(os.path.dirname(model_directory))
supported_modes = model_data["supported_modes"]
name = model_data["name"]
color_modes = get_color_modes(model_directory, data_dir, model_data)
aliases = model_data.get("aliases") or []
rows.append(
[
manufacturer,
model,
name,
",".join(supported_modes),
",".join(color_modes),
",".join(aliases),
]
)
for model in relevant_models:
row = [
model["manufacturer"],
model["model"],
model["name"],
",".join(model["supported_modes"]),
",".join(model.get("aliases") or []),
]
if device_type == "light":
row.append(",".join(model.get("color_modes") or []))
rows.append(row)

rows = sorted(rows, key=lambda x: (x[0], x[1]))
writer.value_matrix = rows
writer.table_name = f"Supported models ({len(rows)} total)"
writer.dump(md_file)
writer.table_name = f"{device_type[1]} ({len(rows)} total)"
output += "\n" + writer.dumps()

md_file = open(os.path.join(PROJECT_ROOT, "docs/supported_models.md"), "w")
md_file.write(output)
md_file.close()

print("Generated supported_models.md")


def get_color_modes(model_directory: str, data_dir: str, model_data: dict) -> list:
def get_model_list() -> list[dict]:
models = []
data_dir = f"{PROJECT_ROOT}/custom_components/powercalc/data"
for json_path in glob.glob(
f"{data_dir}/*/*/model.json",
recursive=True,
):
with open(json_path) as json_file:
model_directory = os.path.dirname(json_path)
model_data: dict = json.load(json_file)
color_modes = get_color_modes(model_directory, data_dir, model_data)
model_data.update(
{
"model": os.path.basename(model_directory),
"manufacturer": os.path.basename(os.path.dirname(model_directory)),
"color_modes": color_modes
}
)
models.append(model_data)

return models


def get_color_modes(model_directory: str, data_dir: str, model_data: dict) -> set:
if "linked_lut" in model_data:
model_directory = os.path.join(data_dir, model_data["linked_lut"])

Expand All @@ -80,12 +108,12 @@ def get_color_modes(model_directory: str, data_dir: str, model_data: dict) -> li
return color_modes


def get_manufacturer_by_directory_name(search_directory: str) -> Optional[str]:
def get_manufacturer_by_directory_name(search_directory: str) -> str | None:
for manufacturer, directory in MANUFACTURER_DIRECTORY_MAPPING.items():
if search_directory == directory:
return manufacturer

return None


generate_supported_model_list()
generate_supported_model_list()
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,20 +216,20 @@ sensor:
```

### Linear mode
Supported domains: `light`, `fan`
Supported domains: `light`, `fan`, `media_player`

The linear mode can be used for dimmable devices which don't have a lookup table available.
You need to supply the min and max power draw yourself, by either looking at the datasheet or measuring yourself with a smart plug / power meter.
Power consumpion is calculated by ratio. So when you have your fan running at 50% speed and define watt range 2 - 6, than the estimated consumption will be 4 watt.

#### Configuration options
| Name | Type | Requirement | Description |
| ----------------- | ------- | ------------ | ------------------------------------------- |
| attribute | string | **Optional** | State attribute to use for the linear range. When not supplied will be `brightness` for lights, and `percentage` for fans |
| min_power | float | **Optional** | Power usage for lowest brightness level |
| max_power | float | **Optional** | Power usage for highest brightness level |
| calibrate | string | **Optional** | Calibration values |
| gamma_curve | float | **Optional** | Apply a gamma correction, for example 2.8 |
| Name | Type | Requirement | Description |
| ----------------- | ------- | ------------ |-------------------------------------------------------------------------------------------------------------------------------------------------------|
| attribute | string | **Optional** | State attribute to use for the linear range. When not supplied will be `brightness` for lights, `percentage` for fans and `volume` for media players. |
| min_power | float | **Optional** | Power usage for lowest brightness level |
| max_power | float | **Optional** | Power usage for highest brightness level |
| calibrate | string | **Optional** | Calibration values |
| gamma_curve | float | **Optional** | Apply a gamma correction, for example 2.8 |

#### Example configuration

Expand Down
3 changes: 2 additions & 1 deletion custom_components/powercalc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import voluptuous as vol
from awesomeversion.awesomeversion import AwesomeVersion
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.components.utility_meter import DEFAULT_OFFSET, max_28_days
Expand Down Expand Up @@ -299,7 +300,7 @@ async def start_discovery(self):
if entity_entry.disabled:
continue

if entity_entry.domain not in (LIGHT_DOMAIN, SWITCH_DOMAIN):
if entity_entry.domain not in (LIGHT_DOMAIN, SWITCH_DOMAIN, MEDIA_PLAYER_DOMAIN):
continue

if not await has_manufacturer_and_model_information(
Expand Down
59 changes: 32 additions & 27 deletions custom_components/powercalc/aliases.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,51 +2,56 @@

from __future__ import annotations

MANUFACTURER_SIGNIFY = "Signify Netherlands B.V."
MANUFACTURER_IKEA = "IKEA of Sweden"
MANUFACTURER_MULLER_LIGHT = "Müller Licht"
MANUFACTURER_YEELIGHT = "Yeelight"
MANUFACTURER_TUYA = "TuYa"
MANUFACTURER_3A_SMARTHOME = "3A Smart Home DE"
MANUFACTURER_AQARA = "Aqara"
MANUFACTURER_LEXMAN = "Lexman"
MANUFACTURER_MELITECH = "MeLiTec"
MANUFACTURER_WIZ = "WiZ"
MANUFACTURER_OSRAM = "OSRAM"
MANUFACTURER_LEDVANCE = "LEDVANCE"
MANUFACTURER_DENON = "Denon"
MANUFACTURER_FEIBIT = "Feibit Inc co. "
MANUFACTURER_GREENWAVE = "GreenWave Reality Inc."
MANUFACTURER_FIBARO = "Fibargroup"
MANUFACTURER_3A_SMARTHOME = "3A Smart Home DE"
MANUFACTURER_GOOGLE = "Google Inc."
MANUFACTURER_GREENWAVE = "GreenWave Reality Inc."
MANUFACTURER_IKEA = "IKEA of Sweden"
MANUFACTURER_LEDVANCE = "LEDVANCE"
MANUFACTURER_LEXMAN = "Lexman"
MANUFACTURER_MELITECH = "MeLiTec"
MANUFACTURER_MULLER_LIGHT = "Müller Licht"
MANUFACTURER_NEO_ELECTRONICS = "Shenzhen Neo Electronics Co., Ltd."
MANUFACTURER_OSRAM = "OSRAM"
MANUFACTURER_SIGNIFY = "Signify Netherlands B.V."
MANUFACTURER_TUYA = "TuYa"
MANUFACTURER_WIZ = "WiZ"
MANUFACTURER_YEELIGHT = "Yeelight"

MANUFACTURER_ALIASES = {
"Philips": MANUFACTURER_SIGNIFY,
"ADEO": MANUFACTURER_LEXMAN,
"Fibaro": MANUFACTURER_FIBARO,
"Greenwave": MANUFACTURER_GREENWAVE,
"HEOS": MANUFACTURER_DENON,
"IKEA": MANUFACTURER_IKEA,
"Xiaomi": MANUFACTURER_AQARA,
"LightZone": MANUFACTURER_MELITECH,
"LUMI": MANUFACTURER_AQARA,
"ADEO": MANUFACTURER_LEXMAN,
"MLI": MANUFACTURER_MULLER_LIGHT,
"LightZone": MANUFACTURER_MELITECH,
"Greenwave": MANUFACTURER_GREENWAVE,
"Fibaro": MANUFACTURER_FIBARO,
"Neo Coolcam": MANUFACTURER_NEO_ELECTRONICS,
"Philips": MANUFACTURER_SIGNIFY,
"Xiaomi": MANUFACTURER_AQARA,
}

MANUFACTURER_DIRECTORY_MAPPING = {
MANUFACTURER_IKEA: "ikea",
MANUFACTURER_3A_SMARTHOME: "3a smarthome",
MANUFACTURER_AQARA: "aqara",
MANUFACTURER_DENON: "denon",
MANUFACTURER_FEIBIT: "jiawen",
MANUFACTURER_FIBARO: "fibaro",
MANUFACTURER_GOOGLE: "google",
MANUFACTURER_GREENWAVE: "greenwave",
MANUFACTURER_IKEA: "ikea",
MANUFACTURER_LEDVANCE: "ledvance",
MANUFACTURER_LEXMAN: "lexman",
MANUFACTURER_MELITECH: "melitec",
MANUFACTURER_MULLER_LIGHT: "mueller-licht",
MANUFACTURER_NEO_ELECTRONICS: "neo-coolcam",
MANUFACTURER_OSRAM: "osram",
MANUFACTURER_SIGNIFY: "signify",
MANUFACTURER_AQARA: "aqara",
MANUFACTURER_LEXMAN: "lexman",
MANUFACTURER_YEELIGHT: "yeelight",
MANUFACTURER_TUYA: "tuya",
MANUFACTURER_MELITECH: "melitec",
MANUFACTURER_WIZ: "wiz",
MANUFACTURER_GREENWAVE: "greenwave",
MANUFACTURER_FIBARO: "fibaro",
MANUFACTURER_3A_SMARTHOME: "3a smarthome",
MANUFACTURER_NEO_ELECTRONICS: "neo-coolcam",
MANUFACTURER_YEELIGHT: "yeelight",
}
2 changes: 2 additions & 0 deletions custom_components/powercalc/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
CONF_CREATE_ENERGY_SENSORS = "create_energy_sensors"
CONF_CREATE_UTILITY_METERS = "create_utility_meters"
CONF_DAILY_FIXED_ENERGY = "daily_fixed_energy"
CONF_DELAY = "delay"
CONF_ENABLE_AUTODISCOVERY = "enable_autodiscovery"
CONF_ENERGY_INTEGRATION_METHOD = "energy_integration_method"
CONF_ENERGY_SENSOR_CATEGORY = "energy_sensor_category"
Expand Down Expand Up @@ -71,6 +72,7 @@
CONF_TEMPLATE = "template"
CONF_SENSOR_TYPE = "sensor_type"
CONF_SUB_PROFILE = "sub_profile"
CONF_SLEEP_POWER = "sleep_power"
CONF_UPDATE_FREQUENCY = "update_frequency"
CONF_VALUE = "value"
CONF_VALUE_TEMPLATE = "value_template"
Expand Down
30 changes: 30 additions & 0 deletions custom_components/powercalc/data/denon/HEOS 1 HS2/model.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"measure_description": "Streamed 'Armin van Buuren - Coming Around Again' using spotify, and took average measurements using the measure.py tool on different volume levels",
"measure_method": "manual",
"measure_device": "Shelly Plug S",
"name": "HEOS 1 HS2",
"standby_power": 3.99,
"device_type": "smart_speaker",
"supported_modes": [
"linear"
],
"calculation_enabled_condition": "{{ is_state('[[entity]]', 'playing') }}",
"linear_config": {
"calibrate": [
"0 -> 6.20",
"10 -> 6.21",
"20 -> 6.25",
"30 -> 6.33",
"40 -> 6.60",
"50 -> 7.20",
"60 -> 7.99",
"70 -> 9.41",
"80 -> 11.35",
"90 -> 12.50",
"100 -> 12.55"
]
},
"aliases": [
"HEOS 1"
]
}
30 changes: 30 additions & 0 deletions custom_components/powercalc/data/google/Home Mini (HOA)/model.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"measure_description": "Streamed 'Armin van Buuren - Come Around Again' using spotify, and took average measurements using the measure.py tool on different volume levels",
"measure_method": "manual",
"measure_device": "Shelly Plug S",
"name": "Google Home Mini",
"standby_power": 1.65,
"device_type": "smart_speaker",
"supported_modes": [
"linear"
],
"calculation_enabled_condition": "{{ is_state('[[entity]]', 'playing') }}",
"linear_config": {
"calibrate": [
"0 -> 1.80",
"10 -> 1.84",
"20 -> 1.87",
"30 -> 1.90",
"40 -> 1.92",
"50 -> 1.97",
"60 -> 2.07",
"70 -> 2.20",
"80 -> 2.35",
"90 -> 2.42",
"100 -> 2.49"
]
},
"aliases": [
"Google Home Mini"
]
}
16 changes: 16 additions & 0 deletions custom_components/powercalc/data/google/Home/model.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"measure_description": "Manually sampled at 10% volume increases, radio, listening to EDM. Max power is from manufacturer specs",
"measure_method": "manual",
"measure_device": "TP-Link Tapo P110",
"name": "Google Home",
"standby_power": 2.4,
"device_type": "smart_speaker",
"supported_modes": [
"linear"
],
"calculation_enabled_condition": "{{ is_state('[[entity]]', 'playing') }}",
"linear_config": {
"min_power": 2.4,
"max_power": 3.87
}
}
Loading

0 comments on commit cf9d046

Please sign in to comment.