diff --git a/README.md b/README.md index 4a34c15..87abf2b 100644 --- a/README.md +++ b/README.md @@ -175,3 +175,107 @@ cards: name: Ajusta ICarga icon: mdi:current-ac ``` + +OR USING pvpc control ( you need to configure pvpc on the integration config ): +``` +type: vertical-stack +cards: + - type: markdown + content:

CARGADOR COCHE

+ - type: horizontal-stack + cards: + - type: gauge + entity: sensor.v2c_trydan_sensor_fvpower + severity: + green: 0 + yellow: 8000 + red: 9500 + needle: true + min: 0 + max: 10000 + name: Producción FV + - type: gauge + entity: sensor.v2c_trydan_sensor_housepower + name: Consumo hogar + severity: + green: -10000 + yellow: 0 + red: 0 + needle: true + min: -10000 + max: 10000 + - type: gauge + entity: sensor.v2c_trydan_sensor_chargepower + name: Cargando en el coche + needle: true + min: 0 + max: 8000 + severity: + green: 0 + yellow: 7200 + red: 7700 + - type: entities + entities: + - entity: sensor.v2c_trydan_sensor_chargestate + name: Estado + secondary_info: none + - entity: switch.v2c_trydan_switch_dynamic + name: Control dinámico + secondary_info: last-changed + - entity: switch.v2c_trydan_switch_paused + name: En pausa + icon: mdi:play + secondary_info: last-changed + - entity: switch.v2c_trydan_switch_locked + name: Bloquedado + icon: mdi:play + secondary_info: last-changed + - entity: sensor.v2c_trydan_sensor_intensity + name: Intensidad de carga + secondary_info: last-changed + - entity: sensor.v2c_trydan_sensor_minintensity + name: Mínimo dinámica + secondary_info: last-changed + - entity: sensor.v2c_trydan_sensor_maxintensity + name: Máximo dinámica + secondary_info: last-changed + - entity: sensor.v2c_trydan_sensor_chargeenergy + name: Energía cargada + secondary_info: none + - entity: sensor.v2c_trydan_sensor_chargekm + name: Km Cargados + secondary_info: none + - entity: sensor.v2c_trydan_sensor_chargetime + name: Tiempo de carga + - entity: number.v2c_km_to_charge + name: Km a Cargar + secondary_info: last-changed + icon: mdi:car-arrow-right + - entity: sensor.v2c_precio_luz + - entity: switch.v2c_trydan_switch_v2c_carga_pvpc + name: Carga por Precio + - entity: number.v2c_maxprice + - type: conditional + conditions: + - entity: switch.v2c_trydan_switch_dynamic + state: 'on' + card: + type: entities + entities: + - entity: number.v2c_min_intensity + name: Ajusta Imin dinámica + icon: mdi:current-ac + - entity: number.v2c_max_intensity + name: Ajusta Imax dinámica + icon: mdi:current-ac + - type: conditional + conditions: + - entity: switch.v2c_trydan_switch_dynamic + state: 'off' + card: + type: entities + entities: + - entity: number.v2c_intensity + name: Ajusta ICarga + icon: mdi:current-ac +``` diff --git a/custom_components/v2c_trydan/config_flow.py b/custom_components/v2c_trydan/config_flow.py index d4788d3..ffaca62 100644 --- a/custom_components/v2c_trydan/config_flow.py +++ b/custom_components/v2c_trydan/config_flow.py @@ -3,7 +3,7 @@ from homeassistant.core import HomeAssistant from homeassistant.const import CONF_IP_ADDRESS -from .const import DOMAIN, CONF_KWH_PER_100KM +from .const import DOMAIN, CONF_KWH_PER_100KM, CONF_PRECIO_LUZ DATA_SCHEMA = vol.Schema( { @@ -46,6 +46,7 @@ class V2CtrydanOptionsFlowHandler(config_entries.OptionsFlow): def __init__(self, config_entry: config_entries.ConfigEntry): self.config_entry = config_entry self.current_kwh_per_100km = config_entry.options.get(CONF_KWH_PER_100KM, 20.8) + self.current_precio_luz = config_entry.options.get(CONF_PRECIO_LUZ, "sensor.precio_luz") async def async_step_init(self, user_input=None): if user_input is not None: @@ -56,6 +57,9 @@ async def async_step_init(self, user_input=None): vol.Required( CONF_KWH_PER_100KM, description={"suggested_value": self.current_kwh_per_100km} ): vol.Coerce(float), + vol.Optional( + CONF_PRECIO_LUZ, description={"suggested_value": self.current_precio_luz} + ): str, } ) diff --git a/custom_components/v2c_trydan/const.py b/custom_components/v2c_trydan/const.py index e555281..6880648 100644 --- a/custom_components/v2c_trydan/const.py +++ b/custom_components/v2c_trydan/const.py @@ -1,4 +1,5 @@ DOMAIN = "v2c_trydan" CONF_IP_ADDRESS = "ip_address" CONF_KWH_PER_100KM = "kwh_per_100km" -CONF_KM_TO_CHARGE = "km_to_charge" \ No newline at end of file +CONF_KM_TO_CHARGE = "km_to_charge" +CONF_PRECIO_LUZ = "precio_luz" \ No newline at end of file diff --git a/custom_components/v2c_trydan/manifest.json b/custom_components/v2c_trydan/manifest.json index 87c5371..6ad4447 100644 --- a/custom_components/v2c_trydan/manifest.json +++ b/custom_components/v2c_trydan/manifest.json @@ -11,6 +11,6 @@ "quality_scale": "internal", "requirements": ["aiohttp"], "ssdp": [], - "version": "2.9.1", + "version": "2.9.2", "zeroconf": [] } \ No newline at end of file diff --git a/custom_components/v2c_trydan/number.py b/custom_components/v2c_trydan/number.py index b8edfaa..e092afc 100644 --- a/custom_components/v2c_trydan/number.py +++ b/custom_components/v2c_trydan/number.py @@ -7,10 +7,12 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, config_entry, async_add_entities): + async_add_entities([MaxIntensityNumber(hass)]) async_add_entities([MinIntensityNumber(hass)]) async_add_entities([KmToChargeNumber(hass)]) - async_add_entities([IntensityNumber(hass)]) + async_add_entities([IntensityNumber(hass)]) + async_add_entities([MaxPrice(hass)]) class MaxIntensityNumber(NumberEntity): def __init__(self, hass): @@ -175,3 +177,42 @@ async def async_set_native_value(self, value): else: _LOGGER.error("v2c_intensity must be between {} and {}".format(self.native_min_value, self.native_max_value)) +class MaxPrice(NumberEntity): + def __init__(self, hass): + self._hass = hass + self._state = 0 + + @property + def unique_id(self): + return "v2c_MaxPrice" + + @property + def name(self): + return "v2c_MaxPrice" + + @property + def icon(self): + return "mdi:currency-eur" + + @property + def native_value(self): + return self._state + + @property + def native_step(self) -> float | None: + return 0.001 + + @property + def native_max_value(self): + return 1.000 + + @property + def native_min_value(self): + return 0.000 + + async def async_set_native_value(self, value): + if 0 <= value <= 1.0: + self._state = value + self.async_write_ha_state() + else: + _LOGGER.error("v2c_MaxPrice must be between 0 and 1") \ No newline at end of file diff --git a/custom_components/v2c_trydan/sensor.py b/custom_components/v2c_trydan/sensor.py index 90d730e..58fb43d 100644 --- a/custom_components/v2c_trydan/sensor.py +++ b/custom_components/v2c_trydan/sensor.py @@ -1,28 +1,43 @@ import logging +import asyncio from datetime import timedelta, datetime import aiohttp import async_timeout import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity, SensorDeviceClass -from homeassistant.const import CONF_IP_ADDRESS -from homeassistant.core import HomeAssistant -from homeassistant.core import callback +from homeassistant.const import ( + CONF_NAME, + STATE_UNKNOWN, + CONF_IP_ADDRESS, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorEntity, + SensorDeviceClass, +) from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers import entity_registry from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import ( + async_track_time_interval, + async_track_state_change, + async_call_later, +) from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, UpdateFailed, ) -from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.event import async_track_state_change -from .const import DOMAIN, CONF_KWH_PER_100KM +from .const import DOMAIN, CONF_KWH_PER_100KM, CONF_PRECIO_LUZ from .coordinator import V2CtrydanDataUpdateCoordinator from .number import KmToChargeNumber +DEPENDENCIES = ["switch"] + _LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -55,6 +70,8 @@ "MaxIntensity": "A" } +UPDATE_INTERVAL = timedelta(minutes=1) + async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entities): ip_address = config_entry.data[CONF_IP_ADDRESS] kwh_per_100km = config_entry.options.get(CONF_KWH_PER_100KM, 15) @@ -67,6 +84,21 @@ async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entitie ] sensors.append(ChargeKmSensor(coordinator, ip_address, kwh_per_100km)) sensors.append(NumericalStatus(coordinator)) + + await asyncio.sleep(10) + + precio_luz_entity = hass.states.get(config_entry.options[CONF_PRECIO_LUZ]) if CONF_PRECIO_LUZ in config_entry.options else None + + async def async_update_precio_luz(now): + nonlocal precio_luz_entity + if precio_luz_entity is not None: + precio_luz_entity = hass.states.get(CONF_PRECIO_LUZ) + + if all([precio_luz_entity]): + precio_luz_entity_instance = PrecioLuzEntity(coordinator, precio_luz_entity, ip_address) + sensors.append(precio_luz_entity_instance) + _LOGGER.debug("PrecioLuzEntity instance added to sensors list") + async_add_entities(sensors) class V2CtrydanSensor(CoordinatorEntity, SensorEntity): @@ -241,16 +273,12 @@ async def async_added_to_hass(self): async def check_and_pause_charging(self, now): paused_switch = self.hass.states.get("switch.v2c_trydan_switch_paused") if paused_switch is not None and paused_switch.state == "on": - #_LOGGER.debug("Charging is paused, skipping check_and_pause_charging") return - #_LOGGER.debug("Checking if it's necessary to pause charging") km_to_charge = self.hass.states.get("number.v2c_km_to_charge") if km_to_charge is not None: km_to_charge = float(km_to_charge.state) - #_LOGGER.debug(f"Current km_to_charge value: {km_to_charge}") if self.state >= km_to_charge and km_to_charge != 0: - #_LOGGER.debug("Pausing charging and resetting km to charge") await self.hass.services.async_call("switch", "turn_on", {"entity_id": "switch.v2c_trydan_switch_paused"}) await self.async_set_km_to_charge(0) self.hass.bus.async_fire("v2c_trydan.charging_complete") @@ -308,4 +336,87 @@ def state(self): @property def state_class(self): - return "measurement" \ No newline at end of file + return "measurement" + +class PrecioLuzEntity(CoordinatorEntity, SensorEntity): + def __init__(self, coordinator, precio_luz_entity, ip_address): + super().__init__(coordinator) + self.v2c_precio_luz_entity = precio_luz_entity + self.ip_address = ip_address + + @property + def unique_id(self): + return f"v2c_precio_luz_entity" + + @property + def name(self): + return "v2c Precio Luz" + + @property + def state(self): + if self.v2c_precio_luz_entity is not None: + return self.v2c_precio_luz_entity.state + else: + return None + + @property + def extra_state_attributes(self): + if self.v2c_precio_luz_entity is not None: + return self.v2c_precio_luz_entity.attributes + else: + return None + + @property + def state_class(self): + if self.v2c_precio_luz_entity is not None: + return self.v2c_precio_luz_entity.attributes.get("state_class", "measurement") + else: + return "measurement" + + @property + def native_unit_of_measurement(self): + if self.v2c_precio_luz_entity is not None: + return self.v2c_precio_luz_entity.attributes.get("unit_of_measurement", "€/kWh") + else: + return "€/kWh" + + async def async_added_to_hass(self): + """Register update callback when added to hass.""" + paused_switch_id = f"{self.ip_address}_Paused" + v2c_carga_pvpc_switch_id = f"v2c_carga_pvpc" + max_price_entity_id = "v2c_MaxPrice" + + async def update_state(event_time): + precio_luz_entity = self.v2c_precio_luz_entity + + entity_registry_instance = entity_registry.async_get(self.hass) + for entity_id, entity_entry in entity_registry_instance.entities.items(): + if entity_entry.unique_id == paused_switch_id: + paused_switch = self.hass.data["switch"].get_entity(entity_id) + if entity_entry.unique_id == v2c_carga_pvpc_switch_id: + v2c_carga_pvpc_switch = self.hass.data["switch"].get_entity(entity_id) + if entity_entry.unique_id == max_price_entity_id: + max_price_entity = self.hass.states.get(entity_id) + + if all([precio_luz_entity, paused_switch, v2c_carga_pvpc_switch, max_price_entity]): + max_price = float(max_price_entity.state) + if v2c_carga_pvpc_switch.is_on: + # Comprueba si el precio está entre max_price + if float(self.state) <= max_price: + self.hass.async_create_task(paused_switch.async_turn_off()) + else: + self.hass.async_create_task(paused_switch.async_turn_on()) + else: + _LOGGER.debug("Hay entidades aun no creadas") + if precio_luz_entity is None: + _LOGGER.debug(f"1 V2C precio_luz_entity: {precio_luz_entity}") + if paused_switch is None: + _LOGGER.debug(f"1 V2C paused_switch: {paused_switch}") + if v2c_carga_pvpc_switch is None: + _LOGGER.debug(f"1 V2C v2c_carga_pvpc_switch: {v2c_carga_pvpc_switch}") + if max_price_entity is None: + _LOGGER.debug(f"1 V2C max_price_entity: {max_price_entity}") + + await update_state(None) + + async_track_time_interval(self.hass, update_state, timedelta(seconds=60)) \ No newline at end of file diff --git a/custom_components/v2c_trydan/switch.py b/custom_components/v2c_trydan/switch.py index ae2ade8..df1a541 100644 --- a/custom_components/v2c_trydan/switch.py +++ b/custom_components/v2c_trydan/switch.py @@ -17,7 +17,7 @@ ) from .coordinator import V2CtrydanDataUpdateCoordinator -from .const import DOMAIN +from .const import DOMAIN, CONF_PRECIO_LUZ _LOGGER = logging.getLogger(__name__) @@ -32,10 +32,16 @@ async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entitie coordinator = V2CtrydanDataUpdateCoordinator(hass, ip_address) await coordinator.async_config_entry_first_refresh() + # Obtén precio_luz_entity + precio_luz_entity = hass.states.get(config_entry.options[CONF_PRECIO_LUZ]) if CONF_PRECIO_LUZ in config_entry.options else None + switches = [ V2CtrydanSwitch(coordinator, ip_address, key) for key in ["Paused", "Dynamic", "Locked"] ] + + switches.append(V2CCargaPVPCSwitch(precio_luz_entity)) + async_add_entities(switches) class V2CtrydanSwitch(CoordinatorEntity, SwitchEntity): @@ -75,3 +81,29 @@ async def async_turn_off(self, **kwargs): await self.coordinator.async_request_refresh() except Exception as e: _LOGGER.error(f"Error turning off switch: {e}") + +class V2CCargaPVPCSwitch(SwitchEntity): + def __init__(self, precio_luz_entity): + self._is_on = False + self.precio_luz_entity = precio_luz_entity + + @property + def unique_id(self): + return f"v2c_carga_pvpc" + + @property + def name(self): + return "V2C trydan Switch v2c_carga_pvpc" + + @property + def is_on(self): + return self._is_on + + async def async_turn_on(self, **kwargs): + if self.precio_luz_entity is not None: + self._is_on = True + else: + self._is_on = False + + async def async_turn_off(self, **kwargs): + self._is_on = False