Skip to content

Commit

Permalink
added EVCC status + BinSensors
Browse files Browse the repository at this point in the history
  • Loading branch information
marq24 committed Dec 3, 2024
1 parent 073ba9e commit 94c78c1
Show file tree
Hide file tree
Showing 9 changed files with 204 additions and 54 deletions.
9 changes: 5 additions & 4 deletions custom_components/tibber_graphapi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,21 @@
from homeassistant.helpers.entity import EntityDescription, Entity
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .const import (
from custom_components.tibber_graphapi.const import (
DOMAIN,
MANUFACTURE,
PLATFORMS,
DEFAULT_SCAN_INTERVAL,
DEFAULT_VEHINDEX_NUMBER,
CONF_VEHINDEX_NUMBER,
CONF_TIBBER_VEHICLE_ID, DEFAULT_VEHINDEX_NUMBER, CONF_TIBBER_VEHICLE_NAME
CONF_TIBBER_VEHICLE_ID,
CONF_TIBBER_VEHICLE_NAME
)

_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=10)
CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA)

PLATFORMS = ["sensor"]

CC_P1: Final = re.compile(r"(.)([A-Z][a-z]+)")
CC_P2: Final = re.compile(r"([a-z0-9])([A-Z])")

Expand Down
76 changes: 76 additions & 0 deletions custom_components/tibber_graphapi/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import logging

from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_OFF
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from custom_components.tibber_graphapi import TibberGraphApiDataUpdateCoordinator, TibberGraphApiEntity
from custom_components.tibber_graphapi.const import (
DOMAIN,
BINARY_SENSORS,
ExtBinarySensorEntityDescription
)

_LOGGER = logging.getLogger(__name__)


async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, add_entity_cb: AddEntitiesCallback):
_LOGGER.debug("BINARY_SENSOR async_setup_entry")
coordinator = hass.data[DOMAIN][config_entry.entry_id]
entities = []
for description in BINARY_SENSORS:
entity = TibberGraphApiBinarySensor(coordinator, description)
entities.append(entity)

add_entity_cb(entities)


class TibberGraphApiBinarySensor(TibberGraphApiEntity, BinarySensorEntity):
def __init__(self, coordinator: TibberGraphApiDataUpdateCoordinator, description: ExtBinarySensorEntityDescription):
super().__init__(coordinator=coordinator, description=description)
self._attr_icon_off = self.entity_description.icon_off

@property
def is_on(self) -> bool | None:
try:
if self.coordinator.data is not None:
if hasattr(self.entity_description, "tag"):
# jpath is set ?! ["key1", "child2"]]
if self.entity_description.tag.jpath is not None and len(self.entity_description.tag.jpath) > 0:
path = self.entity_description.tag.jpath
value = self.get_value_in_path(self.coordinator.data, path)

elif self.entity_description.tag.jkey is not None:
value = self.coordinator.data[self.entity_description.tag.jkey]

# have we a key/value map ?!
if isinstance(value, list) and "key" in value[0]:
if self.entity_description.tag.jvaluekey is not None:
for item in value:
if item["key"] == self.entity_description.tag.jvaluekey:
value = item["value"]

except (IndexError, ValueError, TypeError):
pass

if not isinstance(value, bool):
if isinstance(value, str):
# parse anything else then 'on' to False!
if value.lower() == 'on':
value = True
else:
value = False
else:
value = False

return value

@property
def icon(self):
"""Return the icon of the sensor."""
if self._attr_icon_off is not None and self.state == STATE_OFF:
return self._attr_icon_off
else:
return super().icon
2 changes: 1 addition & 1 deletion custom_components/tibber_graphapi/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from requests.exceptions import HTTPError, Timeout

from custom_components.tibber_graphapi import TibberGraphApiBridge
from .const import (
from custom_components.tibber_graphapi.const import (
DOMAIN,
CONF_VEHINDEX_NUMBER,
CONF_TIBBER_VEHICLE_ID,
Expand Down
59 changes: 48 additions & 11 deletions custom_components/tibber_graphapi/const.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
from custom_components.tibber_graphapi.tags import TGATag
from typing import Final

from homeassistant.components.binary_sensor import BinarySensorEntityDescription
from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import (
PERCENTAGE,
UnitOfLength
UnitOfLength,
EntityCategory
)

DOMAIN: Final = "tibber_graphapi"
MANUFACTURE: Final = "Tibber"

PLATFORMS: Final = ["binary_sensor", "sensor"]

CONF_TIBBER_VEHICLE_ID = "tibber_vehicle_id"
CONF_TIBBER_VEHICLE_NAME = "tibber_vehicle_name"
CONF_VEHINDEX_NUMBER = "vehicle_index_number"
Expand All @@ -26,19 +30,43 @@
# for evcc we need the following sensor types!
# https://docs.evcc.io/docs/devices/vehicles#manuell

class ExtBinarySensorEntityDescription(BinarySensorEntityDescription, frozen_or_thawed=True):
tag: TGATag | None = None
icon_off: str | None = None

class ExtSensorEntityDescription(SensorEntityDescription, frozen_or_thawed=True):
tag: TGATag | None = None

BINARY_SENSORS = [
ExtBinarySensorEntityDescription(
tag=TGATag.VEH_PIN_REQUIRED,
key=TGATag.VEH_PIN_REQUIRED.key,
name="Pin Required",
icon="mdi:lock-alert",
icon_off="mdi:lock-open",
entity_category=EntityCategory.DIAGNOSTIC,
device_class=None
),
ExtBinarySensorEntityDescription(
tag=TGATag.VEH_ALIVE,
key=TGATag.VEH_ALIVE.key,
name="Alive?",
icon="mdi:robot",
icon_off="mdi:robot-dead-outline",
entity_category=EntityCategory.DIAGNOSTIC,
device_class=None
)

]

SENSOR_TYPES = [
ExtSensorEntityDescription(
tag=TGATag.VEH_SOC,
key=TGATag.VEH_SOC.key,
name="Battery Level",
icon="mdi:car-electric-outline",
tag=TGATag.VEH_CHARGING_STATUS,
key=TGATag.VEH_CHARGING_STATUS.key,
name="EVCC charging status Code",
icon="mdi:state-machine",
device_class=None,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
suggested_display_precision=0
state_class=None,
),
ExtSensorEntityDescription(
tag=TGATag.VEH_RANGE,
Expand All @@ -50,11 +78,21 @@ class ExtSensorEntityDescription(SensorEntityDescription, frozen_or_thawed=True)
native_unit_of_measurement=UnitOfLength.KILOMETERS,
suggested_display_precision=0
),
ExtSensorEntityDescription(
tag=TGATag.VEH_SOC,
key=TGATag.VEH_SOC.key,
name="Battery Level",
icon="mdi:car-electric-outline",
device_class=None,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
suggested_display_precision=0
),
ExtSensorEntityDescription(
tag=TGATag.VEH_SOCMAX,
key=TGATag.VEH_SOCMAX.key,
name="SOC MAX",
icon="mdi:car-electric-outline",
icon="mdi:battery-charging-100",
device_class=None,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
Expand All @@ -64,11 +102,10 @@ class ExtSensorEntityDescription(SensorEntityDescription, frozen_or_thawed=True)
tag=TGATag.VEH_SOCMIN,
key=TGATag.VEH_SOCMIN.key,
name="SOC min",
icon="mdi:car-electric-outline",
icon="mdi:battery-charging-outline",
device_class=None,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
suggested_display_precision=0
),

]
71 changes: 52 additions & 19 deletions custom_components/tibber_graphapi/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import StateType

from . import TibberGraphApiDataUpdateCoordinator, TibberGraphApiEntity
from .const import (
from custom_components.tibber_graphapi import TibberGraphApiDataUpdateCoordinator, TibberGraphApiEntity
from custom_components.tibber_graphapi.const import (
DOMAIN,
SENSOR_TYPES
)

from custom_components.tibber_graphapi.tags import TGATag

_LOGGER = logging.getLogger(__name__)

async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities):
_LOGGER.debug("SENSOR async_setup_entry")
coordinator = hass.data[DOMAIN][config_entry.entry_id]
entities = []

Expand Down Expand Up @@ -50,29 +53,59 @@ def get_value_in_path(self, data, keys):
# }
# }

# from https://github.com/evcc-io/evcc/blob/master/api/chargemodestatus.go
# StatusA ChargeStatus = "A" // Fzg. angeschlossen: nein Laden aktiv: nein Ladestation betriebsbereit, Fahrzeug getrennt
# StatusB ChargeStatus = "B" // Fzg. angeschlossen: ja Laden aktiv: nein Fahrzeug verbunden, Netzspannung liegt nicht an
# StatusC ChargeStatus = "C" // Fzg. angeschlossen: ja Laden aktiv: ja Fahrzeug lädt, Netzspannung liegt an
# StatusD ChargeStatus = "D" // Fzg. angeschlossen: ja Laden aktiv: ja Fahrzeug lädt mit externer Belüfungsanforderung (für Blei-Säure-Batterien)
# StatusE ChargeStatus = "E" // Fzg. angeschlossen: ja Laden aktiv: nein Fehler Fahrzeug / Kabel (CP-Kurzschluss, 0V)
# StatusF ChargeStatus = "F" // Fzg. angeschlossen: ja Laden aktiv: nein Fehler EVSE oder Abstecken simulieren (CP-Wake-up, -12V)
def is_not_null(self, value) -> bool:
return value is not None and value != "" and value != "null"

@property
def native_value(self) -> StateType:
try:
if self.coordinator.data is not None:
if hasattr(self.entity_description, "tag"):
if self.entity_description.tag.jpath is not None and len(self.entity_description.tag.jpath) > 0:
path = self.entity_description.tag.jpath
return self.get_value_in_path(self.coordinator.data, path)
elif self.entity_description.tag.jkey is not None:
value = self.coordinator.data[self.entity_description.tag.jkey]
if isinstance(value, list):
if self.entity_description.tag.jvaluekey is not None:
for item in value:
if item["key"] == self.entity_description.tag.jvaluekey:
return item["value"]
# hardcoded charging status A-F
if self.entity_description.tag.key == TGATag.VEH_CHARGING_STATUS.key:

if TGATag.VEH_CHARGING_STATUS.jkey in self.coordinator.data:
# from https://github.com/evcc-io/evcc/blob/master/api/chargemodestatus.go
# StatusA ChargeStatus = "A" // Fzg. angeschlossen: nein Laden aktiv: nein Ladestation betriebsbereit, Fahrzeug getrennt
# StatusB ChargeStatus = "B" // Fzg. angeschlossen: ja Laden aktiv: nein Fahrzeug verbunden, Netzspannung liegt nicht an
# StatusC ChargeStatus = "C" // Fzg. angeschlossen: ja Laden aktiv: ja Fahrzeug lädt, Netzspannung liegt an
# StatusD ChargeStatus = "D" // Fzg. angeschlossen: ja Laden aktiv: ja Fahrzeug lädt mit externer Belüfungsanforderung (für Blei-Säure-Batterien)
# StatusE ChargeStatus = "E" // Fzg. angeschlossen: ja Laden aktiv: nein Fehler Fahrzeug / Kabel (CP-Kurzschluss, 0V)
# StatusF ChargeStatus = "F" // Fzg. angeschlossen: ja Laden aktiv: nein Fehler EVSE oder Abstecken simulieren (CP-Wake-up, -12V)

charging_status = self.coordinator.data[TGATag.VEH_CHARGING_STATUS.jkey].lower()
is_charging_status = charging_status == "charging" or charging_status == "chargingac"

# tibber graph api is very optimistic with charging status
if is_charging_status:
charging_obj = self.coordinator.data["charging"]
has_charger_id = self.is_not_null(charging_obj["chargerId"])
progress_obj = charging_obj["progress"]
has_progress = self.is_not_null(progress_obj["cost"]) or self.is_not_null(progress_obj["energy"]) or self.is_not_null(progress_obj["speed"])

if is_charging_status and (has_charger_id or has_progress):
return "C"
else:
# A or B ????
return "B"
else:
return "A"

else:
# jpath is set ?! ["key1", "child2"]]
if self.entity_description.tag.jpath is not None and len(self.entity_description.tag.jpath) > 0:
path = self.entity_description.tag.jpath
return self.get_value_in_path(self.coordinator.data, path)

elif self.entity_description.tag.jkey is not None:
value = self.coordinator.data[self.entity_description.tag.jkey]

# have we a key/value map ?!
if isinstance(value, list) and "key" in value[0]:
if self.entity_description.tag.jvaluekey is not None:
for item in value:
if item["key"] == self.entity_description.tag.jvaluekey:
return item["value"]

except (IndexError, ValueError, TypeError):
pass
Expand Down
3 changes: 1 addition & 2 deletions custom_components/tibber_graphapi/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]",
"no_data": "[%key:common::config_flow::error::no_data%]",
"unknown_mode": "[%key:common::config_flow::error::unknown_mode%]"
"no_data": "[%key:common::config_flow::error::no_data%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
Expand Down
18 changes: 7 additions & 11 deletions custom_components/tibber_graphapi/tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,8 @@

_LOGGER: logging.Logger = logging.getLogger(__package__)

class CAT(Enum):
CONFIG = "CONF"
STATUS = "STAT"
OTHER = "OTHE"
CONSTANT = "CONS"

class ApiKey(NamedTuple):
key: str
cat: CAT
jpath: list[str] = None
jkey: str = None
jvaluekey: str = None
Expand All @@ -29,7 +22,10 @@ def __hash__(self) -> int:
def __str__(self):
return self.key

VEH_SOC = ApiKey(key="soc", cat=CAT.STATUS, jpath=["battery", "level"])
VEH_RANGE = ApiKey(key="range", cat=CAT.STATUS, jpath=["battery", "estimatedRange"])
VEH_SOCMIN = ApiKey(key="soc_min", cat=CAT.STATUS, jkey="userSettings", jvaluekey="online.vehicle.smartCharging.minChargeLimit")
VEH_SOCMAX = ApiKey(key="soc_max", cat=CAT.STATUS, jkey="userSettings", jvaluekey="online.vehicle.smartCharging.targetBatteryLevel")
VEH_SOC = ApiKey(key="soc", jpath=["battery", "level"])
VEH_RANGE = ApiKey(key="range", jpath=["battery", "estimatedRange"])
VEH_SOCMIN = ApiKey(key="soc_min", jkey="userSettings", jvaluekey="online.vehicle.smartCharging.minChargeLimit")
VEH_SOCMAX = ApiKey(key="soc_max", jkey="userSettings", jvaluekey="online.vehicle.smartCharging.targetBatteryLevel")
VEH_CHARGING_STATUS = ApiKey(key="evcc_charging_code", jkey="chargingStatus")
VEH_PIN_REQUIRED = ApiKey(key="enter_pincode", jkey="enterPincode")
VEH_ALIVE = ApiKey(key="alive", jkey="isAlive")
10 changes: 7 additions & 3 deletions custom_components/tibber_graphapi/translations/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
"login_failed": "Login fehlgeschlagen – bitte Host und/oder Passwort prüfen",
"cannot_connect": "Keine Verbindung möglich",
"unknown": "Unbekannter Fehler",
"no_data": "Es konnten keine Daten abgerufen werden",
"unknown_mode": "Bitte prüfe die IP/Host bzw. die Node-Nummer - wenn Du dir sicher bist, dass Deine Informationen korrekt sind, dann kommuniziert der Tibber Pulse über ein noch nicht implementiertes Protokoll. Bitte erstell doch bitte auf GitHub ein Issue mit entsprechenden DEBUG-Log Informationen - ich benötige Beispieldaten. Danke im Voraus!"
"no_data": "Es konnten keine Daten abgerufen werden"
},
"step": {
"user": {
Expand Down Expand Up @@ -38,11 +37,16 @@
}
},
"entity": {
"binary_sensor": {
"enter_pincode": {"name": "PIN Eingabe notwendig"},
"alive": {"name": "Betriebsbereit"}
},
"sensor": {
"range": {"name": "Reichweite"},
"soc": {"name": "Ladestand"},
"soc_max": {"name": "Ladestand min"},
"soc_min": {"name": "Ladestand max"},
"range": {"name": "Reichweite"}
"evcc_charging_code": {"name": "EVCC Ladestatus-Code [A-F]"}
}
}
}
Loading

0 comments on commit 94c78c1

Please sign in to comment.