Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion custom_components/evsemasterudp/evse_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ def _evse_to_dict(self, evse: EVSE) -> Dict[str, Any]:
'name': evse.config.name or 'EVSEMaster',
'configured_max_electricity': evse.config.max_electricity,
'temperature_unit': evse.config.temperature_unit,
'screen_brightness': evse.config.screen_brightness,
}

# Electrical state
Expand Down Expand Up @@ -222,6 +223,15 @@ async def set_max_current(self, serial: str, amps: int) -> bool:

return await evse.set_max_electricity(amps)

async def set_brightness(self, serial: str, brightness: int) -> bool:
"""Set the screen brightness (0-100)"""
evse = self.communicator.get_evse(serial)
if not evse:
_LOGGER.error(f"EVSE {serial} not found")
return False

return await evse.set_brightness(brightness)

async def set_name(self, serial: str, name: str) -> bool:
"""Set the EVSE name"""
evse = self.communicator.get_evse(serial)
Expand Down Expand Up @@ -284,7 +294,7 @@ def _can_start_charge(self, serial: str) -> bool:
def _record_charge_state_change(self, serial: str) -> None:
"""Record a charge stop (to protect the next start)"""
self._last_charge_change[serial] = datetime.now()
_LOGGER.debug(f"Charge stop recorded for {serial}")
_LOGGER.debug(f"Charge stop recorded for {serial}")

# --- Utility exposure for UI / sensors ---
def get_cooldown_remaining(self, serial: str) -> timedelta:
Expand Down
56 changes: 55 additions & 1 deletion custom_components/evsemasterudp/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ async def async_setup_entry(
entities = [
EVSECurrentControl(coordinator, client, serial, base_name),
EVSEFastChangeProtection(coordinator, client, serial, base_name),
EVSEScreenBrightness(coordinator, client, serial, base_name),
]

async_add_entities(entities)
Expand Down Expand Up @@ -120,4 +121,57 @@ async def async_set_native_value(self, value: float) -> None:
self._protection_minutes = int(value)
# Stocker dans le client pour utilisation par la logique de protection
await self.client.set_fast_change_protection(self.serial, self._protection_minutes)
# Pas besoin de refresh car c'est un paramètre local
# Pas besoin de refresh car c'est un paramètre local


class EVSEScreenBrightness(CoordinatorEntity, NumberEntity):
"""Control for screen backlight brightness"""

def __init__(self, coordinator, client, serial: str, base_name: str):
super().__init__(coordinator)
self.client = client
self.serial = serial
self._attr_name = f"{base_name} Screen Brightness"
self._attr_unique_id = f"{serial}_screen_brightness"
self._attr_icon = "mdi:brightness-6"
self._attr_native_unit_of_measurement = "%"
self._attr_native_min_value = 0
self._attr_native_max_value = 100
self._attr_native_step = 1

self._attr_device_info = {
"identifiers": {(DOMAIN, serial)},
"name": base_name,
"manufacturer": "Oniric75",
"model": "EVSE Master UDP",
}

@property
def evse_data(self):
"""Get EVSE data"""
return self.coordinator.data.get(self.serial, {})

@property
def native_value(self) -> float | None:
"""Return the current brightness value"""
data = self.evse_data
# Default to 50% if not available
return data.get("screen_brightness", 50)

@property
def available(self) -> bool:
"""Return whether the control is available"""
data = self.evse_data
return data.get("online", False) and data.get("logged_in", False)

@property
def native_value(self) -> float | None:
"""Return the current brightness value"""
data = self.evse_data
return data.get("screen_brightness", 50)

async def async_set_native_value(self, value: float) -> None:
"""Set the brightness"""
success = await self.client.set_brightness(self.serial, int(value))
if success:
await self.coordinator.async_request_refresh()
42 changes: 41 additions & 1 deletion custom_components/evsemasterudp/protocol/communicator.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@
Login, LoginResponse, SingleACChargingStatusPublicAuto, SingleACChargingStatusResponse
)

from .datagrams import (
RequestLogin, LoginConfirm, PasswordErrorResponse,
Heading, HeadingResponse, SingleACStatus, SingleACStatusResponse,
CurrentChargeRecord, RequestChargeStatusRecord, ChargeStart, ChargeStop,
SetAndGetOutputElectricity, SetAndGetOutputElectricityResponse,
Login, LoginResponse, SingleACChargingStatusPublicAuto, SingleACChargingStatusResponse,
SetAndGetScreenBrightness, SetAndGetScreenBrightnessResponse
)

_LOGGER = logging.getLogger(__name__)

class EVSEInfo:
Expand Down Expand Up @@ -46,6 +55,7 @@ def __init__(self):
self.offline_charge = 0
self.max_electricity = 6
self.temperature_unit = 1
self.screen_brightness = 50

class EVSEState:
"""Electrical state of an EVSE"""
Expand Down Expand Up @@ -332,7 +342,28 @@ async def set_max_electricity(self, amps: int) -> bool:
except Exception as e:
_LOGGER.error(f"Error while setting current for {self.info.serial}: {e}")
return False

async def set_brightness(self, brightness: int) -> bool:
"""Set the screen brightness (0-100)"""
if not self.is_logged_in():
_LOGGER.error(f"EVSE {self.info.serial} not connected")
return False

try:
_LOGGER.info(f"Setting brightness to {brightness}% for {self.info.serial}")

set_brightness = SetAndGetScreenBrightness()
set_brightness.set_device_serial(self.info.serial)
set_brightness.set_device_password(self.password)
set_brightness.set_brightness(brightness)

await self.send_datagram(set_brightness)
_LOGGER.debug(f"SetAndGetScreenBrightness sent to {self.info.serial}")

return True

except Exception as e:
_LOGGER.error(f"Error while setting brightness for {self.info.serial}: {e}")
return False
async def set_name(self, name: str) -> bool:
"""Set the EVSE name"""
if not self.is_logged_in():
Expand Down Expand Up @@ -502,6 +533,8 @@ async def _process_datagram(self, datagram: Datagram, addr: tuple):
await self._handle_heading(evse, datagram)
elif isinstance(datagram, SetAndGetOutputElectricityResponse):
await self._handle_output_electricity_response(evse, datagram)
elif isinstance(datagram, SetAndGetScreenBrightnessResponse):
await self._handle_screen_brightness_response(evse, datagram)
elif isinstance(datagram, PasswordErrorResponse):
# PasswordErrorResponses are handled in the login() method via _wait_for_response
# Ignore those arriving here to avoid misleading error logs
Expand Down Expand Up @@ -645,6 +678,13 @@ async def _handle_output_electricity_response(self, evse: EVSE, datagram: SetAnd
evse.config.max_electricity = datagram.electricity
await self._notify_callbacks('evse_changed', evse)

async def _handle_screen_brightness_response(self, evse: EVSE, datagram: SetAndGetScreenBrightnessResponse):
"""Handle a screen brightness response"""
_LOGGER.debug(f"Screen brightness response received from {evse.info.serial}: {datagram.brightness}%")
# Update local configuration
evse.config.screen_brightness = datagram.brightness
await self._notify_callbacks('evse_changed', evse)

async def send(self, datagram: Datagram, evse: EVSE) -> int:
"""Send a datagram"""
if not self.running:
Expand Down
47 changes: 46 additions & 1 deletion custom_components/evsemasterudp/protocol/datagrams.py
Original file line number Diff line number Diff line change
Expand Up @@ -735,4 +735,49 @@ def pack_payload(self) -> bytes:

def unpack_payload(self, buffer: bytes) -> None:
if len(buffer) >= 1:
self.offline_enabled = struct.unpack('B', buffer[0:1])[0] == 1
self.offline_enabled = struct.unpack('B', buffer[0:1])[0] == 1

@register_datagram
class SetAndGetScreenBrightness(Datagram):
"""0x8162 (33122) - Set/Get screen brightness (App → EVSE)"""
COMMAND = 0x8162

def __init__(self):
super().__init__()
self.brightness: int = 50 # 0-100 percent

def pack_payload(self) -> bytes:
# Payload: [0x00, 0x02, brightness, 0x00, 0x00, 0x00, 0x00, 0x00]
buffer = bytearray(8)
buffer[0] = 0x00
buffer[1] = 0x02
buffer[2] = self.brightness
# Rest filled with zeros (default)
return bytes(buffer)

def unpack_payload(self, buffer: bytes) -> None:
if len(buffer) >= 3:
self.brightness = buffer[2]

def set_brightness(self, value: int):
"""Set brightness value (0-100)"""
if not (0 <= value <= 100):
raise ValueError("Brightness must be 0-100")
self.brightness = value
return self

@register_datagram
class SetAndGetScreenBrightnessResponse(Datagram):
"""0x0162 (354) - Screen brightness response (EVSE → App)"""
COMMAND = 0x0162

def __init__(self):
super().__init__()
self.brightness: int = 50 # 0-100 percent -- default value

def pack_payload(self) -> bytes:
return b'' # App does not generate this message

def unpack_payload(self, buffer: bytes) -> None:
if len(buffer) >= 3:
self.brightness = buffer[2]