diff --git a/.gitignore b/.gitignore index 2b571c3..ad0c401 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +**/.DS_Store __pycache__/ *.pyc .venv*/ diff --git a/README.md b/README.md index 62db57d..d6cdbbb 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ follow the [instructions for adding a custom repository](https://hacs.xyz/docs/faq/custom_repositories) and then the integration will be available to install like any other. -[![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=nanomad&repository=ChargesplitHomeAssistant&category=integration) +[![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=nanomad&repository=hass-chargesplit&category=integration) ## Configuration diff --git a/custom_components/Chargesplit/const.py b/custom_components/Chargesplit/const.py deleted file mode 100644 index b080181..0000000 --- a/custom_components/Chargesplit/const.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Constants for Chargesplit.""" -# Base component constants -NAME = "Chargesplit Domus" -DOMAIN = "Chargesplit" - - -# Configuration and options -CONF_ENABLED = "enabled" -CONF_CODE = "code" -CONF_SYNC_INTERVAL = "sync_interval" -CHARGEPOINT_SERIAL = "serial" -DEFAULT_SYNC_INTERVAL = 60 # seconds - -CONF_MAX_CHARGING_CURRENT_KEY = "PILOTLIMIT" \ No newline at end of file diff --git a/custom_components/Chargesplit/manifest.json b/custom_components/Chargesplit/manifest.json deleted file mode 100644 index 303c6e7..0000000 --- a/custom_components/Chargesplit/manifest.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "domain": "Chargesplit", - "name": "Chargesplit Domus", - "codeowners": ["@nanomad"], - "config_flow": true, - "dependencies": [], - "documentation": "https://github.com/nanomad/ChargesplitHomeAssistant", - "iot_class": "cloud_polling", - "issue_tracker": "https://github.com/nanomad/ChargesplitHomeAssistant/issues", - "requirements": ["requests>=2.28.0"], - "version": "0.0.8" -} diff --git a/custom_components/Chargesplit/sensor.py b/custom_components/Chargesplit/sensor.py deleted file mode 100644 index 2702caa..0000000 --- a/custom_components/Chargesplit/sensor.py +++ /dev/null @@ -1,292 +0,0 @@ -import logging - -from homeassistant.config_entries import ConfigEntry - -from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorEntity, - SensorEntityDescription, - SensorStateClass, -) - -from homeassistant.const import ( - EntityCategory, - UnitOfElectricCurrent, - UnitOfElectricPotential, - UnitOfEnergy, - UnitOfPower, - UnitOfTemperature, -) - -from .const import DOMAIN -from .entity import ChargesplitEntity -from .coordinator import ChargesplitDataUpdateCoordinator - -_LOGGER: logging.Logger = logging.getLogger(__package__) - - -async def async_setup_entry(hass, entry, async_add_devices): - coordinator = hass.data[DOMAIN][entry.entry_id] - serial = entry.data["serial"] - _LOGGER.info("Setting up ChargeSplit with serial " + serial) - - INSTRUMENTS = [ - # (id, description, key, unit, icon, device_class, state_class, serial, entity_category) - ( - "power_voltagel2", - "Voltage L2", - "VOLT2", - UnitOfElectricPotential.VOLT, - "mdi:lightning-bolt", - SensorDeviceClass.VOLTAGE, - SensorStateClass.MEASUREMENT, - serial, - None, - ), - ( - "power_voltagel1", - "Voltage L1", - "VOLT1", - UnitOfElectricPotential.VOLT, - "mdi:lightning-bolt", - SensorDeviceClass.VOLTAGE, - SensorStateClass.MEASUREMENT, - serial, - None, - ), - ( - "power_voltagel3", - "Voltage L3", - "VOLT3", - UnitOfElectricPotential.VOLT, - "mdi:lightning-bolt", - SensorDeviceClass.VOLTAGE, - SensorStateClass.MEASUREMENT, - serial, - None, - ), - ( - "device_temperature", - "Temperature", - "TEMP", - UnitOfTemperature.CELSIUS, - "mdi:temperature-celsius", - SensorDeviceClass.TEMPERATURE, - SensorStateClass.MEASUREMENT, - serial, - None, - ), - ( - "state_class", - "Wallbox Status", - "STATUS", - None, - "mdi:ev-station", - None, - None, - serial, - None, - ), - ( - "device_model", - "Wallbox Model", - "MODEL", - None, - "mdi:ev-station", - None, - None, - serial, - EntityCategory.DIAGNOSTIC, - ), - ( - "device_firmware", - "Wallbox firmware", - "FWVERS", - None, - "mdi:ev-station", - None, - None, - serial, - EntityCategory.DIAGNOSTIC, - ), - ( - "device_serial", - "Wallbox serial", - "SERIAL", - None, - "mdi:ev-station", - None, - None, - serial, - EntityCategory.DIAGNOSTIC, - ), - ( - "power_charged_kWh", - "Charged kWh", - "TOTALCHARGED", - UnitOfEnergy.KILO_WATT_HOUR, - "mdi:speedometer", - SensorDeviceClass.ENERGY, - SensorStateClass.TOTAL_INCREASING, - serial, - None, - ), - ( - "power_pilotamps", - "Pilot Amps", - "PILOTLIMIT", - UnitOfElectricCurrent.AMPERE, - "mdi:speedometer", - SensorDeviceClass.CURRENT, - SensorStateClass.MEASUREMENT, - serial, - None, - ), - ( - "power_actual_amps", - "Actual Amps", - "AMP", - UnitOfElectricCurrent.AMPERE, - "mdi:current-ac", - SensorDeviceClass.CURRENT, - SensorStateClass.MEASUREMENT, - serial, - None, - ), - ( - "power_solar_power", - "Actual solar power", - "SOLARPWR", - UnitOfPower.KILO_WATT, - "mdi:speedometer", - SensorDeviceClass.POWER, - SensorStateClass.MEASUREMENT, - serial, - None, - ), - ( - "power_house_power", - "Actual House Consumption", - "HOUSEPWR", - UnitOfPower.KILO_WATT, - "mdi:speedometer", - SensorDeviceClass.POWER, - SensorStateClass.MEASUREMENT, - serial, - None, - ), - ( - "power_car_charging", - "Car Charging Power", - "CHARGINGPWR", - UnitOfPower.KILO_WATT, - "mdi:speedometer", - SensorDeviceClass.POWER, - SensorStateClass.MEASUREMENT, - serial, - None, - ), - ( - "house_charged_Wh", - "Daily House Wh", - "DAYHOUSE", - UnitOfEnergy.WATT_HOUR, - "mdi:speedometer", - SensorDeviceClass.ENERGY, - SensorStateClass.TOTAL_INCREASING, - serial, - None, - ), - ( - "solar_charged_Wh", - "Daily Solar Wh", - "DAYSOLAR", - UnitOfEnergy.WATT_HOUR, - "mdi:speedometer", - SensorDeviceClass.ENERGY, - SensorStateClass.TOTAL_INCREASING, - serial, - None, - ), - ( - "schedule_state", - "Schedule", - "SCHEDULE", - None, - "mdi:calendar-clock", - None, - None, - serial, - EntityCategory.DIAGNOSTIC, - ), - ] - - sensors = [ - ChargesplitSensor( - coordinator, entry, id, description, key, unit, icon, device_class, state_class, serial, entity_category - ) - for id, description, key, unit, icon, device_class, state_class, serial, entity_category in INSTRUMENTS - ] - - async_add_devices(sensors, True) - - -class ChargesplitSensor(ChargesplitEntity, SensorEntity): - - def __init__( - self, - coordinator: ChargesplitDataUpdateCoordinator, - entry: ConfigEntry, - id: str, - description: str, - key: str, - unit: str, - icon: str, - device_class: str, - state_class: str, - serial, - entity_category=None, - ): - super().__init__(coordinator, entry) - self._id = f"{serial}-{description}" - self.description = description - self.key = key - self.unit = unit - self._icon = icon - self._device_class = device_class - self._state_class = state_class - self._attr_entity_category = entity_category - - @property - def native_value(self): - if not self.coordinator.data: - return None - return self.coordinator.data.get(self.key) - - @property - def native_unit_of_measurement(self): - return self.unit - - @property - def icon(self): - return self._icon - - @property - def device_class(self): - return self._device_class - - @property - def state_class(self): - return self._state_class - - @property - def name(self): - return f"{self.description}" - - @property - def id(self): - return f"{DOMAIN}_{self._id}" - - @property - def unique_id(self): - return f"{DOMAIN}-{self._id}-{self.coordinator.api.host}" diff --git a/custom_components/Chargesplit/strings.json b/custom_components/Chargesplit/strings.json deleted file mode 100644 index a887056..0000000 --- a/custom_components/Chargesplit/strings.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "config": { - "step": { - "user": { - "title": "Chargesplit", - "data": { - "serial": "Chargepoint Serial Number", - "code": "Authorization Code" - } - } - }, - "error": { - "auth": "Invalid serial or authorization code", - "cannot_connect": "Cannot connect to Chargesplit service. Check your network and try again." - }, - "abort": { - "already_configured": "Device is already configured" - } - } -} diff --git a/custom_components/Chargesplit/translations/en.json b/custom_components/Chargesplit/translations/en.json deleted file mode 100644 index a06593a..0000000 --- a/custom_components/Chargesplit/translations/en.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "config": { - "step": { - "user": { - "title": "Chargesplit Authentication", - "description": "Enter your chargepoint serial and secret", - "data": { - "serial": "Wallbox Serial", - "code": "Wallbox Secret" - } - } - }, - "error": { - "auth": "Serial/secret is wrong." - }, - "abort": { - "single_instance_allowed": "Only a single instance is allowed." - } - }, "options": { - "step": { - "user": { - "title": "Polling time Configuration (NA)", - "data": { - "sync_interval": "Sync Interval to Fetch Data in Seconds" - } - } - } - } -} diff --git a/custom_components/Chargesplit/translations/it.json b/custom_components/Chargesplit/translations/it.json deleted file mode 100644 index 5f2cf15..0000000 --- a/custom_components/Chargesplit/translations/it.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "config": { - "step": { - "user": { - "title": "Chargesplit login", - "description": "Inserisci il seriale della tua wallbox (CH..) ed il codice segreto", - "data": { - "serial": "Seriale Wallbox", - "code": "Codice Segreto" - } - } - }, - "error": { - "auth": "Seriale/codice errati" - }, - "abort": { - "single_instance_allowed": "Puoi installare una singola stazione" - } - }, "options": { - "step": { - "user": { - "title": "Tempo polling in secondi (ND)", - "data": { - "sync_interval": "Valore in secondi" - } - } - } - } - } - \ No newline at end of file diff --git a/custom_components/Chargesplit/__init__.py b/custom_components/chargesplit/__init__.py similarity index 100% rename from custom_components/Chargesplit/__init__.py rename to custom_components/chargesplit/__init__.py diff --git a/custom_components/Chargesplit/api.py b/custom_components/chargesplit/api.py similarity index 75% rename from custom_components/Chargesplit/api.py rename to custom_components/chargesplit/api.py index e11686b..a7f2f58 100644 --- a/custom_components/Chargesplit/api.py +++ b/custom_components/chargesplit/api.py @@ -9,23 +9,16 @@ class ChargesplitApi: def __init__(self, code: str, serial: str) -> None: - self.host = serial self.code = code self.serial = serial def get_data(self) -> bytes: - response = requests.post( - _BASE_URL, - data={"SECRET": self.code, "SERIAL": self.serial}, - ) + response = requests.post(_BASE_URL, data={"SECRET": self.code, "SERIAL": self.serial}) response.raise_for_status() return response.content def test_auth(self) -> None: - response = requests.post( - _BASE_URL, - data={"SECRET": self.code, "SERIAL": self.serial}, - ) + response = requests.post(_BASE_URL, data={"SECRET": self.code, "SERIAL": self.serial}) if response.status_code != 200: raise requests.ConnectionError(f"Auth failed with status {response.status_code}") diff --git a/custom_components/Chargesplit/brand/icon.png b/custom_components/chargesplit/brand/icon.png similarity index 100% rename from custom_components/Chargesplit/brand/icon.png rename to custom_components/chargesplit/brand/icon.png diff --git a/custom_components/Chargesplit/brand/icon@2x.png b/custom_components/chargesplit/brand/icon@2x.png similarity index 100% rename from custom_components/Chargesplit/brand/icon@2x.png rename to custom_components/chargesplit/brand/icon@2x.png diff --git a/custom_components/Chargesplit/config_flow.py b/custom_components/chargesplit/config_flow.py similarity index 100% rename from custom_components/Chargesplit/config_flow.py rename to custom_components/chargesplit/config_flow.py diff --git a/custom_components/chargesplit/const.py b/custom_components/chargesplit/const.py new file mode 100644 index 0000000..2d957ed --- /dev/null +++ b/custom_components/chargesplit/const.py @@ -0,0 +1,9 @@ +"""Constants for Chargesplit.""" +DOMAIN = "chargesplit" + +CONF_CODE = "code" +CONF_SYNC_INTERVAL = "sync_interval" +CHARGEPOINT_SERIAL = "serial" +DEFAULT_SYNC_INTERVAL = 60 # seconds + +CONF_MAX_CHARGING_CURRENT_KEY = "PILOTLIMIT" diff --git a/custom_components/Chargesplit/coordinator.py b/custom_components/chargesplit/coordinator.py similarity index 100% rename from custom_components/Chargesplit/coordinator.py rename to custom_components/chargesplit/coordinator.py diff --git a/custom_components/Chargesplit/entity.py b/custom_components/chargesplit/entity.py similarity index 76% rename from custom_components/Chargesplit/entity.py rename to custom_components/chargesplit/entity.py index 06da3c2..563f54b 100644 --- a/custom_components/Chargesplit/entity.py +++ b/custom_components/chargesplit/entity.py @@ -2,13 +2,15 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, NAME +from .const import DOMAIN from .coordinator import ChargesplitDataUpdateCoordinator _LOGGER: logging.Logger = logging.getLogger(__package__) class ChargesplitEntity(CoordinatorEntity): + _attr_has_entity_name = True + def __init__(self, coordinator: ChargesplitDataUpdateCoordinator, entry): super().__init__(coordinator) self.entry = entry @@ -16,10 +18,11 @@ def __init__(self, coordinator: ChargesplitDataUpdateCoordinator, entry): @property def device_info(self): data = self.coordinator.data or {} + serial = self.coordinator.api.serial return { - "identifiers": {(DOMAIN, self.coordinator.api.host)}, - "name": NAME, - "manufacturer": NAME, + "identifiers": {(DOMAIN, serial)}, + "name": f"Chargesplit {serial}", + "manufacturer": "Chargesplit", "model": data.get("MODEL"), "sw_version": str(data["FWVERS"]) if "FWVERS" in data else None, } diff --git a/custom_components/chargesplit/manifest.json b/custom_components/chargesplit/manifest.json new file mode 100644 index 0000000..094aebe --- /dev/null +++ b/custom_components/chargesplit/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "chargesplit", + "name": "Chargesplit Domus", + "codeowners": ["@nanomad"], + "config_flow": true, + "dependencies": [], + "documentation": "https://github.com/nanomad/hass-chargesplit", + "iot_class": "cloud_polling", + "issue_tracker": "https://github.com/nanomad/hass-chargesplit/issues", + "requirements": ["requests>=2.28.0"], + "version": "0.1.0" +} diff --git a/custom_components/Chargesplit/select.py b/custom_components/chargesplit/select.py similarity index 72% rename from custom_components/Chargesplit/select.py rename to custom_components/chargesplit/select.py index 198b6b5..449908f 100644 --- a/custom_components/Chargesplit/select.py +++ b/custom_components/chargesplit/select.py @@ -7,7 +7,7 @@ from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, NAME +from .const import DOMAIN from .coordinator import ChargesplitDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -24,22 +24,22 @@ OPERATION_MODE = SelectEntityDescription( key="operation_mode", - name="Select Chargepoint Power AMPS", + name="Power Limit", icon="mdi:ev-charger", entity_category=EntityCategory.CONFIG, ) LOCK_MODE = SelectEntityDescription( key="lock_mode", - name="Send Lock/unlock command", - icon="mdi:ev-charger", + name="Lock", + icon="mdi:lock", entity_category=EntityCategory.CONFIG, ) PAUSE_MODE = SelectEntityDescription( key="pause_mode", - name="Send pause/restart command", - icon="mdi:ev-charger", + name="Pause", + icon="mdi:pause-circle", entity_category=EntityCategory.CONFIG, ) @@ -49,7 +49,6 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up the select entities from a config entry.""" coordinator: ChargesplitDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] code = config_entry.data["code"] serial = config_entry.data["serial"] @@ -61,13 +60,27 @@ async def async_setup_entry( ]) -class ChargepointOperationModeEntity(CoordinatorEntity, SelectEntity): - """Entity for selecting the chargepoint power in amps. +class _BaseSelectEntity(SelectEntity): + _attr_should_poll = False + _attr_has_entity_name = True - Extends CoordinatorEntity so _attr_current_option reflects the PILOTLIMIT - value reported by the device after each coordinator refresh. - """ + def __init__(self, description: SelectEntityDescription, serial: str, code: str) -> None: + self.entity_description = description + self._attr_unique_id = f"{serial}_{description.key}" + self._attr_current_option = None + self.serial = serial + self.code = code + @property + def device_info(self) -> DeviceInfo: + return DeviceInfo( + identifiers={(DOMAIN, self.serial)}, + name=f"Chargesplit {self.serial}", + manufacturer="Chargesplit", + ) + + +class ChargepointOperationModeEntity(CoordinatorEntity, _BaseSelectEntity): def __init__( self, description: SelectEntityDescription, @@ -76,11 +89,8 @@ def __init__( coordinator: ChargesplitDataUpdateCoordinator, ) -> None: CoordinatorEntity.__init__(self, coordinator) - self.entity_description = description - self._attr_unique_id = f"{serial}-{description.key}" + _BaseSelectEntity.__init__(self, description, serial, code) self._attr_options = CHARGEPOINT_OPERATION_MODES - self.serial = serial - self.code = code @property def current_option(self) -> str | None: @@ -89,16 +99,7 @@ def current_option(self) -> str | None: value = self.coordinator.data.get("PILOTLIMIT") return str(value) if value is not None else None - @property - def device_info(self) -> DeviceInfo: - return DeviceInfo( - identifiers={(DOMAIN, self.serial)}, - name=NAME, - manufacturer=NAME, - ) - async def async_select_option(self, option: str) -> None: - """Change the selected option.""" data = {"SECRET": self.code, "SERIAL": self.serial, "COMMAND": "PILOTCHANGE", "VALUE": option} try: response = await self.hass.async_add_executor_job( @@ -110,29 +111,12 @@ async def async_select_option(self, option: str) -> None: await self.coordinator.async_request_refresh() -class ChargepointLockModeEntity(SelectEntity): - """Entity for sending lock/unlock commands.""" - - _attr_should_poll = False - +class ChargepointLockModeEntity(_BaseSelectEntity): def __init__(self, description: SelectEntityDescription, serial: str, code: str) -> None: - self.entity_description = description - self._attr_unique_id = f"{serial}-{description.key}" + super().__init__(description, serial, code) self._attr_options = CHARGEPOINT_LOCK_MODES - self._attr_current_option = None - self.serial = serial - self.code = code - - @property - def device_info(self) -> DeviceInfo: - return DeviceInfo( - identifiers={(DOMAIN, self.serial)}, - name=NAME, - manufacturer=NAME, - ) async def async_select_option(self, option: str) -> None: - """Change the selected option.""" data = {"SECRET": self.code, "SERIAL": self.serial, "COMMAND": "LOCK", "VALUE": option} try: response = await self.hass.async_add_executor_job( @@ -145,29 +129,12 @@ async def async_select_option(self, option: str) -> None: self.async_write_ha_state() -class ChargepointPauseModeEntity(SelectEntity): - """Entity for sending pause/restart commands.""" - - _attr_should_poll = False - +class ChargepointPauseModeEntity(_BaseSelectEntity): def __init__(self, description: SelectEntityDescription, serial: str, code: str) -> None: - self.entity_description = description - self._attr_unique_id = f"{serial}-{description.key}" + super().__init__(description, serial, code) self._attr_options = CHARGEPOINT_PAUSE_MODES - self._attr_current_option = None - self.serial = serial - self.code = code - - @property - def device_info(self) -> DeviceInfo: - return DeviceInfo( - identifiers={(DOMAIN, self.serial)}, - name=NAME, - manufacturer=NAME, - ) async def async_select_option(self, option: str) -> None: - """Change the selected option.""" data = {"SECRET": self.code, "SERIAL": self.serial, "COMMAND": "PAUSERESTART", "VALUE": option} try: response = await self.hass.async_add_executor_job( diff --git a/custom_components/chargesplit/sensor.py b/custom_components/chargesplit/sensor.py new file mode 100644 index 0000000..945ce14 --- /dev/null +++ b/custom_components/chargesplit/sensor.py @@ -0,0 +1,88 @@ +import logging + +from homeassistant.config_entries import ConfigEntry + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorStateClass, +) + +from homeassistant.const import ( + EntityCategory, + UnitOfElectricCurrent, + UnitOfElectricPotential, + UnitOfEnergy, + UnitOfPower, + UnitOfTemperature, +) + +from .const import DOMAIN +from .entity import ChargesplitEntity +from .coordinator import ChargesplitDataUpdateCoordinator + +_LOGGER: logging.Logger = logging.getLogger(__package__) + +# (unique_id_suffix, name, data_key, unit, icon, device_class, state_class, entity_category) +INSTRUMENTS = [ + ("voltage_l1", "Voltage L1", "VOLT1", UnitOfElectricPotential.VOLT, "mdi:lightning-bolt", SensorDeviceClass.VOLTAGE, SensorStateClass.MEASUREMENT, None), + ("voltage_l2", "Voltage L2", "VOLT2", UnitOfElectricPotential.VOLT, "mdi:lightning-bolt", SensorDeviceClass.VOLTAGE, SensorStateClass.MEASUREMENT, None), + ("voltage_l3", "Voltage L3", "VOLT3", UnitOfElectricPotential.VOLT, "mdi:lightning-bolt", SensorDeviceClass.VOLTAGE, SensorStateClass.MEASUREMENT, None), + ("temperature", "Temperature", "TEMP", UnitOfTemperature.CELSIUS, "mdi:thermometer", SensorDeviceClass.TEMPERATURE, SensorStateClass.MEASUREMENT, None), + ("status", "Wallbox Status", "STATUS", None, "mdi:ev-station", None, None, None), + ("model", "Model", "MODEL", None, "mdi:information-outline", None, None, EntityCategory.DIAGNOSTIC), + ("firmware", "Firmware", "FWVERS", None, "mdi:chip", None, None, EntityCategory.DIAGNOSTIC), + ("serial", "Serial", "SERIAL", None, "mdi:barcode", None, None, EntityCategory.DIAGNOSTIC), + ("total_charged_kwh", "Total Charged", "TOTALCHARGED", UnitOfEnergy.KILO_WATT_HOUR, "mdi:battery-charging", SensorDeviceClass.ENERGY, SensorStateClass.TOTAL_INCREASING, None), + ("pilot_amps", "Pilot Amps", "PILOTLIMIT", UnitOfElectricCurrent.AMPERE, "mdi:current-ac", SensorDeviceClass.CURRENT, SensorStateClass.MEASUREMENT, None), + ("actual_amps", "Actual Amps", "AMP", UnitOfElectricCurrent.AMPERE, "mdi:current-ac", SensorDeviceClass.CURRENT, SensorStateClass.MEASUREMENT, None), + ("solar_power", "Solar Power", "SOLARPWR", UnitOfPower.KILO_WATT, "mdi:solar-power", SensorDeviceClass.POWER, SensorStateClass.MEASUREMENT, None), + ("house_power", "House Consumption", "HOUSEPWR", UnitOfPower.KILO_WATT, "mdi:home-lightning-bolt", SensorDeviceClass.POWER, SensorStateClass.MEASUREMENT, None), + ("charging_power", "Charging Power", "CHARGINGPWR", UnitOfPower.KILO_WATT, "mdi:ev-plug-type2", SensorDeviceClass.POWER, SensorStateClass.MEASUREMENT, None), + ("daily_house_wh", "Daily House Energy", "DAYHOUSE", UnitOfEnergy.WATT_HOUR, "mdi:home-lightning-bolt", SensorDeviceClass.ENERGY, SensorStateClass.TOTAL_INCREASING, None), + ("daily_solar_wh", "Daily Solar Energy", "DAYSOLAR", UnitOfEnergy.WATT_HOUR, "mdi:solar-power", SensorDeviceClass.ENERGY, SensorStateClass.TOTAL_INCREASING, None), + ("schedule", "Schedule", "SCHEDULE", None, "mdi:calendar-clock", None, None, EntityCategory.DIAGNOSTIC), +] + + +async def async_setup_entry(hass, entry: ConfigEntry, async_add_devices): + coordinator = hass.data[DOMAIN][entry.entry_id] + serial = entry.data["serial"] + + async_add_devices([ + ChargesplitSensor(coordinator, entry, serial, *instrument) + for instrument in INSTRUMENTS + ], True) + + +class ChargesplitSensor(ChargesplitEntity, SensorEntity): + + def __init__( + self, + coordinator: ChargesplitDataUpdateCoordinator, + entry: ConfigEntry, + serial: str, + uid_suffix: str, + name: str, + key: str, + unit: str, + icon: str, + device_class, + state_class, + entity_category, + ): + super().__init__(coordinator, entry) + self._attr_unique_id = f"{serial}_{uid_suffix}" + self._attr_name = name + self._attr_icon = icon + self._attr_device_class = device_class + self._attr_state_class = state_class + self._attr_native_unit_of_measurement = unit + self._attr_entity_category = entity_category + self.key = key + + @property + def native_value(self): + if not self.coordinator.data: + return None + return self.coordinator.data.get(self.key) diff --git a/custom_components/chargesplit/strings.json b/custom_components/chargesplit/strings.json new file mode 100644 index 0000000..e114034 --- /dev/null +++ b/custom_components/chargesplit/strings.json @@ -0,0 +1,30 @@ +{ + "config": { + "step": { + "user": { + "title": "Chargesplit", + "data": { + "serial": "Wallbox Serial", + "code": "Wallbox Secret" + } + } + }, + "error": { + "auth": "Serial or secret is incorrect.", + "cannot_connect": "Cannot connect to Chargesplit service. Check your network and try again." + }, + "abort": { + "already_configured": "A wallbox with this serial is already configured." + } + }, + "options": { + "step": { + "user": { + "title": "Polling Configuration", + "data": { + "sync_interval": "Sync Interval (seconds)" + } + } + } + } +} diff --git a/custom_components/chargesplit/translations/en.json b/custom_components/chargesplit/translations/en.json new file mode 100644 index 0000000..f5685a7 --- /dev/null +++ b/custom_components/chargesplit/translations/en.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "user": { + "title": "Chargesplit Authentication", + "description": "Enter your wallbox serial and secret", + "data": { + "serial": "Wallbox Serial", + "code": "Wallbox Secret" + } + } + }, + "error": { + "auth": "Serial or secret is incorrect.", + "cannot_connect": "Cannot connect to Chargesplit service. Check your network and try again." + }, + "abort": { + "already_configured": "A wallbox with this serial is already configured." + } + }, + "options": { + "step": { + "user": { + "title": "Polling Configuration", + "data": { + "sync_interval": "Sync Interval (seconds)" + } + } + } + } +} diff --git a/custom_components/chargesplit/translations/it.json b/custom_components/chargesplit/translations/it.json new file mode 100644 index 0000000..018ad14 --- /dev/null +++ b/custom_components/chargesplit/translations/it.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "user": { + "title": "Chargesplit login", + "description": "Inserisci il seriale della tua wallbox (CH..) ed il codice segreto", + "data": { + "serial": "Seriale Wallbox", + "code": "Codice Segreto" + } + } + }, + "error": { + "auth": "Seriale o codice errati.", + "cannot_connect": "Impossibile connettersi al servizio Chargesplit. Verifica la rete e riprova." + }, + "abort": { + "already_configured": "Una wallbox con questo seriale è già configurata." + } + }, + "options": { + "step": { + "user": { + "title": "Configurazione polling", + "data": { + "sync_interval": "Intervallo di sincronizzazione (secondi)" + } + } + } + } +} diff --git a/tests/test_coordinator.py b/tests/test_coordinator.py index 6fdc5eb..f118eba 100644 --- a/tests/test_coordinator.py +++ b/tests/test_coordinator.py @@ -1,6 +1,6 @@ import logging -from custom_components.Chargesplit.coordinator import _coerce_numeric +from custom_components.chargesplit.coordinator import _coerce_numeric def test_casts_strings_and_ints_to_their_target_types(): diff --git a/tests/test_setup.py b/tests/test_setup.py deleted file mode 100644 index 2b44f55..0000000 --- a/tests/test_setup.py +++ /dev/null @@ -1,375 +0,0 @@ -"""v0.0.7 setup contract. - -Captures the entity registry, device registry, and a few state-value -spot checks produced by the v0.0.7 codebase against a scrubbed fixture. - -The v0.1.0 migration test on the pr-9 branch should NOT re-use this -pattern verbatim. The right shape for that test is: pre-populate the -entity + device registries with the EXPECTED_V007 / EXPECTED_DEVICES -rows below, THEN call async_setup under v0.1.0 code, THEN assert -nothing was renamed, orphaned, or had its statistics-keying attributes -flipped (state_class, unit_of_measurement, device_class). Setting up a -fresh entry under v0.1.0 doesn't exercise migration. -""" - -from operator import itemgetter -from pathlib import Path -from unittest.mock import patch - -from homeassistant.helpers import device_registry as dr, entity_registry as er -from pytest_homeassistant_custom_component.common import MockConfigEntry - -from custom_components.Chargesplit.api import ChargesplitApi - -FIXTURE = Path(__file__).parent / "fixtures" / "wallbox_response.json" - - -def _entity_snapshot(hass, e): - state = hass.states.get(e.entity_id) - attrs = state.attributes if state else {} - return { - "entity_id": e.entity_id, - "unique_id": e.unique_id, - "platform": e.platform, - "original_name": e.original_name, - "entity_category": e.entity_category.value if e.entity_category else None, - "disabled_by": e.disabled_by.value if e.disabled_by else None, - "has_entity_name": e.has_entity_name, - "device_class": attrs.get("device_class"), - "unit_of_measurement": attrs.get("unit_of_measurement"), - "state_class": attrs.get("state_class"), - } - - -def _device_snapshot(d): - return { - "identifiers": sorted(d.identifiers), - "manufacturer": d.manufacturer, - "model": d.model, - "sw_version": d.sw_version, - "name": d.name, - } - - -async def test_setup_produces_correct_entities(hass): - data = FIXTURE.read_bytes() - - entry = MockConfigEntry( - version=1, - domain="Chargesplit", - data={"serial": "TESTSERIAL", "code": "TESTSECRET"}, - title="TESTSERIAL", - ) - entry.add_to_hass(hass) - - with patch.object(ChargesplitApi, "get_data", return_value=data): - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - entity_registry = er.async_get(hass) - registry_entries = [ - e for e in entity_registry.entities.values() - if e.config_entry_id == entry.entry_id - ] - entries = [_entity_snapshot(hass, e) for e in registry_entries] - - device_registry = dr.async_get(hass) - devices = [ - _device_snapshot(d) - for d in device_registry.devices.values() - if entry.entry_id in d.config_entries - ] - - # All entities attach to a single device. Implicit in EXPECTED_DEVICES - # having one row, but explicit is clearer and gives a better error if a - # future change accidentally splits entities across devices. - device_ids = {e.device_id for e in registry_entries} - assert len(device_ids) == 1, f"expected entities to share one device, got {device_ids}" - - assert sorted(entries, key=itemgetter("unique_id")) == sorted( - EXPECTED_V007, key=itemgetter("unique_id") - ) - assert sorted(devices, key=itemgetter("identifiers")) == sorted( - EXPECTED_DEVICES, key=itemgetter("identifiers") - ) - - # State-value assertions: every sensor's JSON-key -> state wiring is locked. - for entity_id, expected in EXPECTED_STATES.items(): - state = hass.states.get(entity_id) - assert state is not None, f"{entity_id} did not register" - assert state.state == expected, f"{entity_id}: expected {expected!r}, got {state.state!r}" - - -EXPECTED_V007 = [ - { - "entity_id": "sensor.chargesplit_domus_actual_amps", - "unique_id": "Chargesplit-TESTSERIAL-Actual Amps-TESTSERIAL", - "platform": "Chargesplit", - "original_name": "Actual Amps", - "entity_category": None, - "disabled_by": None, - "has_entity_name": False, - "device_class": "current", - "unit_of_measurement": "A", - "state_class": "measurement", - }, - { - "entity_id": "sensor.chargesplit_domus_actual_house_consumption", - "unique_id": "Chargesplit-TESTSERIAL-Actual House Consumption-TESTSERIAL", - "platform": "Chargesplit", - "original_name": "Actual House Consumption", - "entity_category": None, - "disabled_by": None, - "has_entity_name": False, - "device_class": "power", - "unit_of_measurement": "kW", - "state_class": "measurement", - }, - { - "entity_id": "sensor.chargesplit_domus_actual_solar_power", - "unique_id": "Chargesplit-TESTSERIAL-Actual solar power-TESTSERIAL", - "platform": "Chargesplit", - "original_name": "Actual solar power", - "entity_category": None, - "disabled_by": None, - "has_entity_name": False, - "device_class": "power", - "unit_of_measurement": "kW", - "state_class": "measurement", - }, - { - "entity_id": "sensor.chargesplit_domus_car_charging_power", - "unique_id": "Chargesplit-TESTSERIAL-Car Charging Power-TESTSERIAL", - "platform": "Chargesplit", - "original_name": "Car Charging Power", - "entity_category": None, - "disabled_by": None, - "has_entity_name": False, - "device_class": "power", - "unit_of_measurement": "kW", - "state_class": "measurement", - }, - { - "entity_id": "sensor.chargesplit_domus_charged_kwh", - "unique_id": "Chargesplit-TESTSERIAL-Charged kWh-TESTSERIAL", - "platform": "Chargesplit", - "original_name": "Charged kWh", - "entity_category": None, - "disabled_by": None, - "has_entity_name": False, - "device_class": "energy", - "unit_of_measurement": "kWh", - "state_class": "total_increasing", - }, - { - "entity_id": "sensor.chargesplit_domus_daily_house_wh", - "unique_id": "Chargesplit-TESTSERIAL-Daily House Wh-TESTSERIAL", - "platform": "Chargesplit", - "original_name": "Daily House Wh", - "entity_category": None, - "disabled_by": None, - "has_entity_name": False, - "device_class": "energy", - "unit_of_measurement": "Wh", - "state_class": "total_increasing", - }, - { - "entity_id": "sensor.chargesplit_domus_daily_solar_wh", - "unique_id": "Chargesplit-TESTSERIAL-Daily Solar Wh-TESTSERIAL", - "platform": "Chargesplit", - "original_name": "Daily Solar Wh", - "entity_category": None, - "disabled_by": None, - "has_entity_name": False, - "device_class": "energy", - "unit_of_measurement": "Wh", - "state_class": "total_increasing", - }, - { - "entity_id": "sensor.chargesplit_domus_pilot_amps", - "unique_id": "Chargesplit-TESTSERIAL-Pilot Amps-TESTSERIAL", - "platform": "Chargesplit", - "original_name": "Pilot Amps", - "entity_category": None, - "disabled_by": None, - "has_entity_name": False, - "device_class": "current", - "unit_of_measurement": "A", - "state_class": "measurement", - }, - { - "entity_id": "sensor.chargesplit_domus_schedule", - "unique_id": "Chargesplit-TESTSERIAL-Schedule-TESTSERIAL", - "platform": "Chargesplit", - "original_name": "Schedule", - "entity_category": "diagnostic", - "disabled_by": None, - "has_entity_name": False, - "device_class": None, - "unit_of_measurement": None, - "state_class": None, - }, - { - "entity_id": "sensor.chargesplit_domus_temperature", - "unique_id": "Chargesplit-TESTSERIAL-Temperature-TESTSERIAL", - "platform": "Chargesplit", - "original_name": "Temperature", - "entity_category": None, - "disabled_by": None, - "has_entity_name": False, - "device_class": "temperature", - "unit_of_measurement": "°C", - "state_class": "measurement", - }, - { - "entity_id": "sensor.chargesplit_domus_voltage_l1", - "unique_id": "Chargesplit-TESTSERIAL-Voltage L1-TESTSERIAL", - "platform": "Chargesplit", - "original_name": "Voltage L1", - "entity_category": None, - "disabled_by": None, - "has_entity_name": False, - "device_class": "voltage", - "unit_of_measurement": "V", - "state_class": "measurement", - }, - { - "entity_id": "sensor.chargesplit_domus_voltage_l2", - "unique_id": "Chargesplit-TESTSERIAL-Voltage L2-TESTSERIAL", - "platform": "Chargesplit", - "original_name": "Voltage L2", - "entity_category": None, - "disabled_by": None, - "has_entity_name": False, - "device_class": "voltage", - "unit_of_measurement": "V", - "state_class": "measurement", - }, - { - "entity_id": "sensor.chargesplit_domus_voltage_l3", - "unique_id": "Chargesplit-TESTSERIAL-Voltage L3-TESTSERIAL", - "platform": "Chargesplit", - "original_name": "Voltage L3", - "entity_category": None, - "disabled_by": None, - "has_entity_name": False, - "device_class": "voltage", - "unit_of_measurement": "V", - "state_class": "measurement", - }, - { - "entity_id": "sensor.chargesplit_domus_wallbox_model", - "unique_id": "Chargesplit-TESTSERIAL-Wallbox Model-TESTSERIAL", - "platform": "Chargesplit", - "original_name": "Wallbox Model", - "entity_category": "diagnostic", - "disabled_by": None, - "has_entity_name": False, - "device_class": None, - "unit_of_measurement": None, - "state_class": None, - }, - { - "entity_id": "sensor.chargesplit_domus_wallbox_status", - "unique_id": "Chargesplit-TESTSERIAL-Wallbox Status-TESTSERIAL", - "platform": "Chargesplit", - "original_name": "Wallbox Status", - "entity_category": None, - "disabled_by": None, - "has_entity_name": False, - "device_class": None, - "unit_of_measurement": None, - "state_class": None, - }, - { - "entity_id": "sensor.chargesplit_domus_wallbox_firmware", - "unique_id": "Chargesplit-TESTSERIAL-Wallbox firmware-TESTSERIAL", - "platform": "Chargesplit", - "original_name": "Wallbox firmware", - "entity_category": "diagnostic", - "disabled_by": None, - "has_entity_name": False, - "device_class": None, - "unit_of_measurement": None, - "state_class": None, - }, - { - "entity_id": "sensor.chargesplit_domus_wallbox_serial", - "unique_id": "Chargesplit-TESTSERIAL-Wallbox serial-TESTSERIAL", - "platform": "Chargesplit", - "original_name": "Wallbox serial", - "entity_category": "diagnostic", - "disabled_by": None, - "has_entity_name": False, - "device_class": None, - "unit_of_measurement": None, - "state_class": None, - }, - { - "entity_id": "select.chargesplit_domus_send_lock_unlock_command", - "unique_id": "TESTSERIAL-lock_mode", - "platform": "Chargesplit", - "original_name": "Send Lock/unlock command", - "entity_category": "config", - "disabled_by": None, - "has_entity_name": False, - "device_class": None, - "unit_of_measurement": None, - "state_class": None, - }, - { - "entity_id": "select.chargesplit_domus_select_chargepoint_power_amps", - "unique_id": "TESTSERIAL-operation_mode", - "platform": "Chargesplit", - "original_name": "Select Chargepoint Power AMPS", - "entity_category": "config", - "disabled_by": None, - "has_entity_name": False, - "device_class": None, - "unit_of_measurement": None, - "state_class": None, - }, - { - "entity_id": "select.chargesplit_domus_send_pause_restart_command", - "unique_id": "TESTSERIAL-pause_mode", - "platform": "Chargesplit", - "original_name": "Send pause/restart command", - "entity_category": "config", - "disabled_by": None, - "has_entity_name": False, - "device_class": None, - "unit_of_measurement": None, - "state_class": None, - }, -] - -EXPECTED_DEVICES = [ - { - "identifiers": [("Chargesplit", "TESTSERIAL")], - "manufacturer": "Chargesplit Domus", - "model": "WB132H", - "sw_version": "2.34", - "name": "Chargesplit Domus", - }, -] - -EXPECTED_STATES = { - "sensor.chargesplit_domus_actual_amps": "0.0", - "sensor.chargesplit_domus_actual_house_consumption": "0.52", - "sensor.chargesplit_domus_actual_solar_power": "0.52", - "sensor.chargesplit_domus_car_charging_power": "0.0", - "sensor.chargesplit_domus_charged_kwh": "0.0", - # DAYHOUSE in the fixture is 5450.409999999997; HA rounds for display/stats. - "sensor.chargesplit_domus_daily_house_wh": "5450.41", - "sensor.chargesplit_domus_daily_solar_wh": "0.0", - "sensor.chargesplit_domus_pilot_amps": "25", - "sensor.chargesplit_domus_schedule": "1", - "sensor.chargesplit_domus_temperature": "21.2", - "sensor.chargesplit_domus_voltage_l1": "0.0", - "sensor.chargesplit_domus_voltage_l2": "0.0", - "sensor.chargesplit_domus_voltage_l3": "0.0", - "sensor.chargesplit_domus_wallbox_firmware": "2.34", - "sensor.chargesplit_domus_wallbox_model": "WB132H", - "sensor.chargesplit_domus_wallbox_serial": "TESTSERIAL", - "sensor.chargesplit_domus_wallbox_status": "SCHEDULE", -} diff --git a/tests/test_setup_contract.py b/tests/test_setup_contract.py new file mode 100644 index 0000000..cb8f0f8 --- /dev/null +++ b/tests/test_setup_contract.py @@ -0,0 +1,375 @@ +"""v0.1.0 setup contract. + +Captures the entity registry, device registry, and state-value spot checks +produced by v0.1.0 on a *fresh* install against the scrubbed fixture. + +This is the post-rename baseline. The companion migration test in +`test_migration.py` covers what existing v0.0.x installs end up with after +auto-migration — and intentionally diverges from this contract on the +entity_id field, because migration preserves the v0.0.7 entity_ids +(statistics history is keyed on entity_id) while a fresh install computes +new ones from `has_entity_name=True` + the v0.1.0 names. +""" + +from operator import itemgetter +from pathlib import Path +from unittest.mock import patch + +from homeassistant.helpers import device_registry as dr, entity_registry as er +from pytest_homeassistant_custom_component.common import MockConfigEntry + +from custom_components.chargesplit.api import ChargesplitApi + +FIXTURE = Path(__file__).parent / "fixtures" / "wallbox_response.json" + + +def _entity_snapshot(hass, e): + state = hass.states.get(e.entity_id) + attrs = state.attributes if state else {} + return { + "entity_id": e.entity_id, + "unique_id": e.unique_id, + "platform": e.platform, + "original_name": e.original_name, + "entity_category": e.entity_category.value if e.entity_category else None, + "disabled_by": e.disabled_by.value if e.disabled_by else None, + "has_entity_name": e.has_entity_name, + "device_class": attrs.get("device_class"), + "unit_of_measurement": attrs.get("unit_of_measurement"), + "state_class": attrs.get("state_class"), + } + + +def _device_snapshot(d): + return { + "identifiers": sorted(d.identifiers), + "manufacturer": d.manufacturer, + "model": d.model, + "sw_version": d.sw_version, + "name": d.name, + } + + +async def test_v010_fresh_install_contract(hass): + data = FIXTURE.read_bytes() + + entry = MockConfigEntry( + version=1, + domain="chargesplit", + data={"serial": "TESTSERIAL", "code": "TESTSECRET"}, + title="TESTSERIAL", + ) + entry.add_to_hass(hass) + + with patch.object(ChargesplitApi, "get_data", return_value=data): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + registry_entries = [ + e + for e in entity_registry.entities.values() + if e.config_entry_id == entry.entry_id + ] + entries = [_entity_snapshot(hass, e) for e in registry_entries] + + device_registry = dr.async_get(hass) + devices = [ + _device_snapshot(d) + for d in device_registry.devices.values() + if entry.entry_id in d.config_entries + ] + + device_ids = {e.device_id for e in registry_entries} + assert ( + len(device_ids) == 1 + ), f"expected entities to share one device, got {device_ids}" + + assert sorted(entries, key=itemgetter("unique_id")) == sorted( + EXPECTED_V010, key=itemgetter("unique_id") + ) + assert sorted(devices, key=itemgetter("identifiers")) == sorted( + EXPECTED_DEVICES, key=itemgetter("identifiers") + ) + + for entity_id, expected in EXPECTED_STATES.items(): + state = hass.states.get(entity_id) + assert state is not None, f"{entity_id} did not register" + assert ( + state.state == expected + ), f"{entity_id}: expected {expected!r}, got {state.state!r}" + + +EXPECTED_V010 = [ + { + "entity_id": "sensor.chargesplit_testserial_actual_amps", + "unique_id": "TESTSERIAL_actual_amps", + "platform": "chargesplit", + "original_name": "Actual Amps", + "entity_category": None, + "disabled_by": None, + "has_entity_name": True, + "device_class": "current", + "unit_of_measurement": "A", + "state_class": "measurement", + }, + { + "entity_id": "sensor.chargesplit_testserial_charging_power", + "unique_id": "TESTSERIAL_charging_power", + "platform": "chargesplit", + "original_name": "Charging Power", + "entity_category": None, + "disabled_by": None, + "has_entity_name": True, + "device_class": "power", + "unit_of_measurement": "kW", + "state_class": "measurement", + }, + { + "entity_id": "sensor.chargesplit_testserial_daily_house_energy", + "unique_id": "TESTSERIAL_daily_house_wh", + "platform": "chargesplit", + "original_name": "Daily House Energy", + "entity_category": None, + "disabled_by": None, + "has_entity_name": True, + "device_class": "energy", + "unit_of_measurement": "Wh", + "state_class": "total_increasing", + }, + { + "entity_id": "sensor.chargesplit_testserial_daily_solar_energy", + "unique_id": "TESTSERIAL_daily_solar_wh", + "platform": "chargesplit", + "original_name": "Daily Solar Energy", + "entity_category": None, + "disabled_by": None, + "has_entity_name": True, + "device_class": "energy", + "unit_of_measurement": "Wh", + "state_class": "total_increasing", + }, + { + "entity_id": "sensor.chargesplit_testserial_firmware", + "unique_id": "TESTSERIAL_firmware", + "platform": "chargesplit", + "original_name": "Firmware", + "entity_category": "diagnostic", + "disabled_by": None, + "has_entity_name": True, + "device_class": None, + "unit_of_measurement": None, + "state_class": None, + }, + { + "entity_id": "sensor.chargesplit_testserial_house_consumption", + "unique_id": "TESTSERIAL_house_power", + "platform": "chargesplit", + "original_name": "House Consumption", + "entity_category": None, + "disabled_by": None, + "has_entity_name": True, + "device_class": "power", + "unit_of_measurement": "kW", + "state_class": "measurement", + }, + { + "entity_id": "select.chargesplit_testserial_lock", + "unique_id": "TESTSERIAL_lock_mode", + "platform": "chargesplit", + "original_name": "Lock", + "entity_category": "config", + "disabled_by": None, + "has_entity_name": True, + "device_class": None, + "unit_of_measurement": None, + "state_class": None, + }, + { + "entity_id": "sensor.chargesplit_testserial_model", + "unique_id": "TESTSERIAL_model", + "platform": "chargesplit", + "original_name": "Model", + "entity_category": "diagnostic", + "disabled_by": None, + "has_entity_name": True, + "device_class": None, + "unit_of_measurement": None, + "state_class": None, + }, + { + "entity_id": "select.chargesplit_testserial_power_limit", + "unique_id": "TESTSERIAL_operation_mode", + "platform": "chargesplit", + "original_name": "Power Limit", + "entity_category": "config", + "disabled_by": None, + "has_entity_name": True, + "device_class": None, + "unit_of_measurement": None, + "state_class": None, + }, + { + "entity_id": "select.chargesplit_testserial_pause", + "unique_id": "TESTSERIAL_pause_mode", + "platform": "chargesplit", + "original_name": "Pause", + "entity_category": "config", + "disabled_by": None, + "has_entity_name": True, + "device_class": None, + "unit_of_measurement": None, + "state_class": None, + }, + { + "entity_id": "sensor.chargesplit_testserial_pilot_amps", + "unique_id": "TESTSERIAL_pilot_amps", + "platform": "chargesplit", + "original_name": "Pilot Amps", + "entity_category": None, + "disabled_by": None, + "has_entity_name": True, + "device_class": "current", + "unit_of_measurement": "A", + "state_class": "measurement", + }, + { + "entity_id": "sensor.chargesplit_testserial_schedule", + "unique_id": "TESTSERIAL_schedule", + "platform": "chargesplit", + "original_name": "Schedule", + "entity_category": "diagnostic", + "disabled_by": None, + "has_entity_name": True, + "device_class": None, + "unit_of_measurement": None, + "state_class": None, + }, + { + "entity_id": "sensor.chargesplit_testserial_serial", + "unique_id": "TESTSERIAL_serial", + "platform": "chargesplit", + "original_name": "Serial", + "entity_category": "diagnostic", + "disabled_by": None, + "has_entity_name": True, + "device_class": None, + "unit_of_measurement": None, + "state_class": None, + }, + { + "entity_id": "sensor.chargesplit_testserial_solar_power", + "unique_id": "TESTSERIAL_solar_power", + "platform": "chargesplit", + "original_name": "Solar Power", + "entity_category": None, + "disabled_by": None, + "has_entity_name": True, + "device_class": "power", + "unit_of_measurement": "kW", + "state_class": "measurement", + }, + { + "entity_id": "sensor.chargesplit_testserial_wallbox_status", + "unique_id": "TESTSERIAL_status", + "platform": "chargesplit", + "original_name": "Wallbox Status", + "entity_category": None, + "disabled_by": None, + "has_entity_name": True, + "device_class": None, + "unit_of_measurement": None, + "state_class": None, + }, + { + "entity_id": "sensor.chargesplit_testserial_temperature", + "unique_id": "TESTSERIAL_temperature", + "platform": "chargesplit", + "original_name": "Temperature", + "entity_category": None, + "disabled_by": None, + "has_entity_name": True, + "device_class": "temperature", + "unit_of_measurement": "°C", + "state_class": "measurement", + }, + { + "entity_id": "sensor.chargesplit_testserial_total_charged", + "unique_id": "TESTSERIAL_total_charged_kwh", + "platform": "chargesplit", + "original_name": "Total Charged", + "entity_category": None, + "disabled_by": None, + "has_entity_name": True, + "device_class": "energy", + "unit_of_measurement": "kWh", + "state_class": "total_increasing", + }, + { + "entity_id": "sensor.chargesplit_testserial_voltage_l1", + "unique_id": "TESTSERIAL_voltage_l1", + "platform": "chargesplit", + "original_name": "Voltage L1", + "entity_category": None, + "disabled_by": None, + "has_entity_name": True, + "device_class": "voltage", + "unit_of_measurement": "V", + "state_class": "measurement", + }, + { + "entity_id": "sensor.chargesplit_testserial_voltage_l2", + "unique_id": "TESTSERIAL_voltage_l2", + "platform": "chargesplit", + "original_name": "Voltage L2", + "entity_category": None, + "disabled_by": None, + "has_entity_name": True, + "device_class": "voltage", + "unit_of_measurement": "V", + "state_class": "measurement", + }, + { + "entity_id": "sensor.chargesplit_testserial_voltage_l3", + "unique_id": "TESTSERIAL_voltage_l3", + "platform": "chargesplit", + "original_name": "Voltage L3", + "entity_category": None, + "disabled_by": None, + "has_entity_name": True, + "device_class": "voltage", + "unit_of_measurement": "V", + "state_class": "measurement", + }, +] + +EXPECTED_DEVICES = [ + { + "identifiers": [("chargesplit", "TESTSERIAL")], + "manufacturer": "Chargesplit", + "model": "WB132H", + "sw_version": "2.34", + "name": "Chargesplit TESTSERIAL", + }, +] + +EXPECTED_STATES = { + "sensor.chargesplit_testserial_voltage_l1": "0.0", + "sensor.chargesplit_testserial_voltage_l2": "0.0", + "sensor.chargesplit_testserial_voltage_l3": "0.0", + "sensor.chargesplit_testserial_temperature": "21.2", + "sensor.chargesplit_testserial_wallbox_status": "SCHEDULE", + "sensor.chargesplit_testserial_model": "WB132H", + "sensor.chargesplit_testserial_firmware": "2.34", + "sensor.chargesplit_testserial_serial": "TESTSERIAL", + "sensor.chargesplit_testserial_total_charged": "0.0", + "sensor.chargesplit_testserial_pilot_amps": "25", + "sensor.chargesplit_testserial_actual_amps": "0.0", + "sensor.chargesplit_testserial_solar_power": "0.52", + "sensor.chargesplit_testserial_house_consumption": "0.52", + "sensor.chargesplit_testserial_charging_power": "0.0", + "sensor.chargesplit_testserial_daily_house_energy": "5450.41", + "sensor.chargesplit_testserial_daily_solar_energy": "0.0", + "sensor.chargesplit_testserial_schedule": "1", + "select.chargesplit_testserial_power_limit": "25", +}