Skip to content

Commit

Permalink
Added goe_charger
Browse files Browse the repository at this point in the history
  • Loading branch information
0xFEEDC0DE64 committed Sep 13, 2021
1 parent 37d75e8 commit 86533e3
Show file tree
Hide file tree
Showing 13 changed files with 926 additions and 0 deletions.
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ homeassistant/components/github/* @timmo001 @ludeeus
homeassistant/components/gitter/* @fabaff
homeassistant/components/glances/* @fabaff @engrbm87
homeassistant/components/goalzero/* @tkdrob
homeassistant/components/goe_charger/* @0xFEEDC0DE64
homeassistant/components/gogogate2/* @vangorra @bdraco
homeassistant/components/google_assistant/* @home-assistant/cloud
homeassistant/components/google_cloud/* @lufton
Expand Down
91 changes: 91 additions & 0 deletions homeassistant/components/goe_charger/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""The go-e Charger integration."""
from __future__ import annotations

from datetime import timedelta
import logging

from homeassistant.core import HomeAssistant
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.helpers import device_registry

from .const import DOMAIN
from .common import GoeChargerHub

_LOGGER = logging.getLogger(__name__)

PLATFORMS: list[str] = ["binary_sensor", "number", "select", "sensor"]

async def async_setup(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up the go-eCharger integration."""

hass.data[DOMAIN] = {}

return True

async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up go-e Charger from a config entry."""

async def async_update_data():
"""Fetch data from API endpoint."""
hub = GoeChargerHub(config_entry.data["host"])

try:
data = await hub.get_data(hass, ["alw","acu","adi","sse","eto","ccw","rssi","lmo","amp","fna","car","err","cbl","wh","fwv","oem","typ","tma","nrg","modelStatus","var","fhz","ust","acs","frc","psm","loc"])

dr = await device_registry.async_get_registry(hass)
dr.async_get_or_create(
name=data["fna"],
config_entry_id=config_entry.entry_id,
#connections={(device_registry.CONNECTION_NETWORK_MAC, "11:22:33:44:55:66")},
identifiers={(DOMAIN, config_entry.data["serial"])},
manufacturer=data["oem"],
model=data["typ"] + " (" + str(data["var"]) + "kW)",
#suggested_area="Kitchen",
sw_version=data["fwv"],
)

return data
except Exception as e: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception %s", str(e))
return None

coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
name="goe_charger_" + config_entry.data["serial"],
update_method=async_update_data,
update_interval=timedelta(seconds=5),
)

hass.data[DOMAIN][config_entry.entry_id] = coordinator

await coordinator.async_config_entry_first_refresh()

if coordinator.data is None:
dr = await device_registry.async_get_registry(hass)
dr.async_get_or_create(
name="go-e_Charger_" + config_entry.data["serial"],
config_entry_id=config_entry.entry_id,
#connections={(device_registry.CONNECTION_NETWORK_MAC, "11:22:33:44:55:66")},
identifiers={(DOMAIN, config_entry.data["serial"])},
manufacturer="<unknown>",
model="<unknown>",
#suggested_area="Kitchen",
sw_version="<unknown>",
)

hass.config_entries.async_setup_platforms(config_entry, PLATFORMS)

return True


async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Unload a config entry."""
if not await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS):
logging.warning('unload platforms failed')
return False;

hass.data[DOMAIN].pop(config_entry.entry_id)

return True
69 changes: 69 additions & 0 deletions homeassistant/components/goe_charger/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Platform for number integration."""
from __future__ import annotations

import logging

from homeassistant.core import HomeAssistant
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.update_coordinator import CoordinatorEntity, DataUpdateCoordinator
from homeassistant.components.binary_sensor import BinarySensorEntity

from .const import DOMAIN

_LOGGER = logging.getLogger(__name__)

async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities) -> None:
coordinator = hass.data[DOMAIN][config_entry.entry_id]
serial = config_entry.data["serial"]

async_add_entities([
GoeChargerBinary(coordinator, "Allow charging", serial, "allow_charging", "alw"),
GoeChargerBinary(coordinator, "Adapter used", serial, "adapter_used", "adi"),
])

class GoeChargerBinary(CoordinatorEntity, BinarySensorEntity):
"""Representation of a Sensor."""

def __init__(self, coordinator: DataUpdateCoordinator, name: str, serial: str, unique_id: str, key: str):
"""Pass coordinator to CoordinatorEntity."""
super().__init__(coordinator)
self._name = name
self._serial = serial
self._unique_id = unique_id
self._key = key

@property
def name(self):
"""Return the name of the device."""
return self._name

@property
def unique_id(self):
"""Return the unique id of the device."""
return "goe_charger_" + self._serial + "_" + self._unique_id

@property
def available(self) -> bool:
"""Return True if entity is available."""
return self.coordinator.data is not None and self._key in self.coordinator.data

@property
def is_on(self) -> bool | None:
"""Return the state of the sensor."""
return None if not self.available else self.coordinator.data[self._key]

async def async_update(self):
"""Fetch new state data for the sensor.
This is the only method that should fetch new data for Home Assistant.
"""
await self.coordinator.async_request_refresh()

@property
def device_info(self):
"""Get attributes about the device."""
return {
"identifiers": {(DOMAIN, self._serial)},
#"name": self._device.label,
#"model": self._device.device_type_name,
#"manufacturer": "Unavailable",
}
144 changes: 144 additions & 0 deletions homeassistant/components/goe_charger/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
"""Common code go-e Charger integration."""
from __future__ import annotations

import logging
from typing import Any

from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession

import aiohttp
import asyncio
import async_timeout
import json
import urllib.parse

_LOGGER = logging.getLogger(__name__)

class GoeChargerHub:
def __init__(self, host: str) -> None:
"""Initialize."""
self._host = host

async def get_data(self, hass: HomeAssistant, keys: list[str]) -> bool:
"""Get the data from the charger."""

url = 'http://' + self._host + '/api/status?filter=' + urllib.parse.quote_plus(','.join(keys))

session = async_get_clientsession(hass)

try:
with async_timeout.timeout(10):
resp = await session.get(url)
content = await resp.text()
except asyncio.TimeoutError:
_LOGGER.warning("Request timeout")
raise TimeoutOccured(url)
except aiohttp.ClientError:
_LOGGER.warning("Request exception")
raise CannotConnect(url)

if resp.status != 200:
_LOGGER.warning("Request invalid response %i %s", resp.status, content)
raise InvalidRespStatus(resp.status, content)

try:
parsed = json.loads(content)
except Exception as e: # pylint: disable=broad-except
details = "Could not parse json " + str(e)
_LOGGER.warning("%s %s", details, content)
raise InvalidJson(details, content)

if type(parsed) is not dict:
details = "json is not a dict ({})".format(type(parsed).__name__)
_LOGGER.warning("%s", details)
raise InvalidJson(details, content)

for key in keys:
if key not in parsed:
details = key + " not set in json object"
_LOGGER.warning("%s", details)
raise InvalidJson(details, content)

_LOGGER.debug("Data received successfully for %s!", self._host)

return parsed

async def set_data(self, hass: HomeAssistant, data: dict[str, Any]) -> None:
"""Set data to the charger."""

url = 'http://' + self._host + '/api/set?'
for key, value in data.items():
url += urllib.parse.quote_plus(key) + '=' + urllib.parse.quote_plus(json.dumps(value)) + '&'

session = async_get_clientsession(hass)

try:
with async_timeout.timeout(10):
resp = await session.get(url)
content = await resp.text()
except asyncio.TimeoutError:
_LOGGER.warning("Request timeout")
raise TimeoutOccured(url)
except aiohttp.ClientError:
_LOGGER.warning("Request exception")
raise CannotConnect(url)

if resp.status != 200:
_LOGGER.warning("Request invalid response %i %s", resp.status, content)
raise InvalidRespStatus(resp.status, content)

try:
parsed = json.loads(content)
except Exception as e: # pylint: disable=broad-except
details = "Could not parse json " + str(e)
_LOGGER.warning("%s %s", details, content)
raise InvalidJson(details, content)

if type(parsed) is not dict:
details = "json is not a dict ({})".format(type(parsed).__name__)
_LOGGER.warning("%s", details)
raise InvalidJson(details, content)

for key in data:
if key not in parsed:
details = key + " not set in json object"
_LOGGER.warning("%s", details)
raise InvalidJson(details, content)

if parsed[key] != True:
details = key + parsed[key]
_LOGGER.warning("%s", details)
raise InvalidJson(details, content)

_LOGGER.debug("Data set successfully for %s!", self._host)

class CannotConnect(HomeAssistantError):
"""Error to indicate we cannot connect."""
def __init__(self, url: str) -> None:
"""Initialize."""
self.url = url

class TimeoutOccured(HomeAssistantError):
"""Error to indicate we cannot connect."""
def __init__(self, url: str) -> None:
"""Initialize."""
self.url = url

class InvalidRespStatus(HomeAssistantError):
"""Error to indicate we got an invalid response status."""
def __init__(self, status: int, response: str) -> None:
"""Initialize."""
self.status = status
self.response = response

class InvalidJson(HomeAssistantError):
"""Error to indicate we got an invalid json response."""
def __init__(self, details: str, response: str) -> None:
"""Initialize."""
self.details = details
self.response = response

class NotImplemented(HomeAssistantError):
"""Error to indicate that something is not yet implemented."""
Loading

0 comments on commit 86533e3

Please sign in to comment.