diff --git a/custom_components/evsemasterudp/evse_client.py b/custom_components/evsemasterudp/evse_client.py index 12d13b3..d804249 100644 --- a/custom_components/evsemasterudp/evse_client.py +++ b/custom_components/evsemasterudp/evse_client.py @@ -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 @@ -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) @@ -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: diff --git a/custom_components/evsemasterudp/number.py b/custom_components/evsemasterudp/number.py index 7e9a2c3..8c36bf8 100644 --- a/custom_components/evsemasterudp/number.py +++ b/custom_components/evsemasterudp/number.py @@ -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) @@ -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 \ No newline at end of file + # 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() \ No newline at end of file diff --git a/custom_components/evsemasterudp/protocol/communicator.py b/custom_components/evsemasterudp/protocol/communicator.py index 0098b4e..96b0e4e 100644 --- a/custom_components/evsemasterudp/protocol/communicator.py +++ b/custom_components/evsemasterudp/protocol/communicator.py @@ -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: @@ -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""" @@ -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(): @@ -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 @@ -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: diff --git a/custom_components/evsemasterudp/protocol/datagrams.py b/custom_components/evsemasterudp/protocol/datagrams.py index c16d63a..5844680 100644 --- a/custom_components/evsemasterudp/protocol/datagrams.py +++ b/custom_components/evsemasterudp/protocol/datagrams.py @@ -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 \ No newline at end of file + 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] \ No newline at end of file