Skip to content

Commit

Permalink
feat: add LED brightness control (#404)
Browse files Browse the repository at this point in the history
* feat: add LED brightness control

* formatting

* linting

* update test

* formatting again
  • Loading branch information
firstof9 authored Nov 15, 2024
1 parent 2a1901f commit f4e1e23
Show file tree
Hide file tree
Showing 8 changed files with 260 additions and 8 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ Platform | Description
-- | --
`binary_sensor` | On/Off sensors for various settings.
`button` | Button to restart the ESP chip or the EVSE.
`light` | Set the LED brightness.
`number` | Set the charge rate.
`select` | Select the ampers limit and service level.
`sensor` | Show info from an OpenEVSE charger's API.
`switch` | Switch to toggle various charger modes.
Expand Down
19 changes: 19 additions & 0 deletions custom_components/openevse/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
DOMAIN,
FW_COORDINATOR,
ISSUE_URL,
LIGHT_TYPES,
MANAGER,
PLATFORMS,
SELECT_TYPES,
Expand Down Expand Up @@ -388,6 +389,24 @@ def parse_sensors(self) -> None:
select,
)
data.update(_sensor)
for light in LIGHT_TYPES: # pylint: disable=consider-using-dict-items
_sensor = {}
try:
sensor_property = LIGHT_TYPES[light].key
# Data can be sent as boolean or as 1/0
_sensor[light] = getattr(self._manager, sensor_property)
_LOGGER.debug(
"light: %s sensor_property: %s value %s",
select,
sensor_property,
_sensor[light],
)
except (ValueError, KeyError):
_LOGGER.info(
"Could not update status for %s",
light,
)
data.update(_sensor)
_LOGGER.debug("DEBUG: %s", data)
self._data = data

Expand Down
26 changes: 19 additions & 7 deletions custom_components/openevse/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from homeassistant.components.switch import SwitchDeviceClass
from homeassistant.const import (
PERCENTAGE,
Platform,
SIGNAL_STRENGTH_DECIBELS,
UnitOfElectricCurrent,
UnitOfElectricPotential,
Expand All @@ -33,6 +34,7 @@
OpenEVSESelectEntityDescription,
OpenEVSESwitchEntityDescription,
OpenEVSENumberEntityDescription,
OpenEVSELightEntityDescription,
)

# config flow
Expand All @@ -55,13 +57,14 @@
VERSION = "1.0.0"
ISSUE_URL = "http://github.com/firstof9/openevse/"
PLATFORMS = [
"binary_sensor",
"button",
"number",
"sensor",
"select",
"switch",
"update",
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.LIGHT,
Platform.NUMBER,
Platform.SENSOR,
Platform.SELECT,
Platform.SWITCH,
Platform.UPDATE,
]
USER_AGENT = "Home Assistant"
MANAGER = "manager"
Expand Down Expand Up @@ -510,3 +513,12 @@
mode=NumberMode.AUTO,
),
}

LIGHT_TYPES: Final[dict[str, OpenEVSELightEntityDescription]] = {
"led_brightness": OpenEVSELightEntityDescription(
key="led_brightness",
name="LED Brightness",
entity_category=EntityCategory.CONFIG,
command="set_led_brightness",
),
}
8 changes: 8 additions & 0 deletions custom_components/openevse/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from dataclasses import dataclass

from homeassistant.components.light import LightEntityDescription
from homeassistant.components.number import NumberEntityDescription
from homeassistant.components.select import SelectEntityDescription
from homeassistant.components.switch import SwitchEntityDescription
Expand Down Expand Up @@ -32,3 +33,10 @@ class OpenEVSENumberEntityDescription(NumberEntityDescription):
default_options: list | None = None
min: int | None = None
max: int | None = None


@dataclass
class OpenEVSELightEntityDescription(LightEntityDescription):
"""Class describing OpenEVSE light entities."""

command: str | None = None
111 changes: 111 additions & 0 deletions custom_components/openevse/light.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
"""Support for OpenEVSE controls using the light platform."""

from __future__ import annotations

import logging
from typing import Any

from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ColorMode,
LightEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import CONF_NAME, COORDINATOR, DOMAIN, LIGHT_TYPES, MANAGER
from .entity import OpenEVSELightEntityDescription

from . import (
OpenEVSEManager,
OpenEVSEUpdateCoordinator,
)

_LOGGER = logging.getLogger(__name__)
DEFAULT_ON = 125
DEFAULT_OFF = 0


async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up OpenEVSE Number entity from Config Entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR]
manager = hass.data[DOMAIN][config_entry.entry_id][MANAGER]

entities: list[LightEntity] = []

for light in LIGHT_TYPES: # pylint: disable=consider-using-dict-items
entities.append(
OpenEVSELight(config_entry, coordinator, LIGHT_TYPES[light], manager)
)
async_add_entities(entities)


class OpenEVSELight(CoordinatorEntity, LightEntity):
"""Implementation of an OpenEVSE light."""

_attr_supported_color_modes = {ColorMode.BRIGHTNESS}
_attr_color_mode = ColorMode.BRIGHTNESS

def __init__(
self,
config: ConfigEntry,
coordinator: OpenEVSEUpdateCoordinator,
light_description: OpenEVSELightEntityDescription,
manager: OpenEVSEManager,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self._config = config
self.entity_description = light_description
self._name = light_description.name
self._type = light_description.key
self._unique_id = config.entry_id
self._command = light_description.command
self._data = coordinator.data
self.coordinator = coordinator
self.manager = manager

self._attr_name = f"{self._config.data[CONF_NAME]} {self._name}"
self._attr_unique_id = f"{self._name}_{self._unique_id}"
self._attr_brightness = coordinator.data[self._type]

@property
def device_info(self) -> dict:
"""Return a port description for device registry."""
info = {
"manufacturer": "OpenEVSE",
"name": self._config.data[CONF_NAME],
"connections": {(DOMAIN, self._unique_id)},
}

return info

@property
def brightness(self) -> int | None:
"""Return the brightness of this light between 0..255."""
self._attr_brightness = self.coordinator.data[self._type]
return self._attr_brightness

@property
def is_on(self) -> bool:
"""Return true if light is on."""
return bool(self._attr_brightness != 0)

async def async_turn_on(self, **kwargs: Any) -> None:
"""Instruct the light to turn on."""
brightness = kwargs.get(ATTR_BRIGHTNESS, self.brightness)

if ATTR_BRIGHTNESS in kwargs:
await self.manager.set_led_brightness(brightness)
return
await self.manager.set_led_brightness(DEFAULT_ON)

async def async_turn_off(self, **kwargs: Any) -> None:
"""Instruct the light to turn off."""
await self.manager.set_led_brightness(DEFAULT_OFF)
2 changes: 1 addition & 1 deletion custom_components/openevse/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"iot_class": "local_push",
"issue_tracker": "https://github.com/firstof9/openevse/issues",
"loggers": ["openevsehttp"],
"requirements": ["python-openevse-http==0.1.62"],
"requirements": ["python-openevse-http==0.1.63"],
"version": "0.0.0-dev",
"zeroconf": ["_openevse._tcp.local."]
}
1 change: 1 addition & 0 deletions tests/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"using_ethernet": False,
"shaper_active": False,
"max_current_soft": 48,
"led_brightness": 128,
}
CONFIG_DATA = {
"name": "openevse",
Expand Down
99 changes: 99 additions & 0 deletions tests/test_light.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""Provide tests for OpenEVSE light platform."""

from datetime import timedelta
from unittest.mock import patch

import pytest
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN, ATTR_BRIGHTNESS
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN
from homeassistant.components.select import DOMAIN as SELECT_DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.helpers import entity_registry as er
from homeassistant.util import dt as dt_util
from pytest_homeassistant_custom_component.common import MockConfigEntry

from custom_components.openevse.const import DOMAIN

from .const import CONFIG_DATA
from .conftest import TEST_URL_CONFIG

pytestmark = pytest.mark.asyncio

CHARGER_NAME = "openevse"


async def test_light(
hass,
test_charger,
mock_ws_start,
mock_aioclient,
):
"""Test setup_entry."""
entry = MockConfigEntry(
domain=DOMAIN,
title=CHARGER_NAME,
data=CONFIG_DATA,
)

entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()

assert len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 4
assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 21
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 4
assert len(hass.states.async_entity_ids(SELECT_DOMAIN)) == 2
assert len(hass.states.async_entity_ids(NUMBER_DOMAIN)) == 1
assert len(hass.states.async_entity_ids(LIGHT_DOMAIN)) == 1
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1

assert DOMAIN in hass.config.components

entity_id = "light.openevse_led_brightness"
state = hass.states.get(entity_id)
assert state
assert state.state == "on"
assert state.attributes[ATTR_BRIGHTNESS] == 128

mock_aioclient.post(
TEST_URL_CONFIG,
status=200,
body='{"msg": "Ok"}',
repeat=True,
)

await hass.services.async_call(
LIGHT_DOMAIN,
"turn_off",
{"entity_id": entity_id},
blocking=True,
)

mock_aioclient.assert_any_call(
TEST_URL_CONFIG, method="POST", data={ATTR_BRIGHTNESS: 0}
)

await hass.services.async_call(
LIGHT_DOMAIN,
"turn_on",
{"entity_id": entity_id},
blocking=True,
)

mock_aioclient.assert_any_call(
TEST_URL_CONFIG, method="POST", data={ATTR_BRIGHTNESS: 128}
)

await hass.services.async_call(
LIGHT_DOMAIN,
"turn_on",
{"entity_id": entity_id, "brightness": 26},
blocking=True,
)

mock_aioclient.assert_any_call(
TEST_URL_CONFIG, method="POST", data={ATTR_BRIGHTNESS: 26}
)

0 comments on commit f4e1e23

Please sign in to comment.