Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Solax Inverter Sensor Component #22579

Merged
merged 15 commits into from May 26, 2019
Merged
1 change: 1 addition & 0 deletions .coveragerc
Expand Up @@ -534,6 +534,7 @@ omit =
homeassistant/components/sochain/sensor.py
homeassistant/components/socialblade/sensor.py
homeassistant/components/solaredge/sensor.py
homeassistant/components/solax/sensor.py
homeassistant/components/sonarr/sensor.py
homeassistant/components/songpal/media_player.py
homeassistant/components/sonos/*
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/solax/__init__.py
@@ -0,0 +1 @@
"""The solax component."""
213 changes: 213 additions & 0 deletions homeassistant/components/solax/sensor.py
@@ -0,0 +1,213 @@
"""Support for Solax inverter via local API."""
import asyncio
import json

from datetime import timedelta
import logging

import aiohttp
import async_timeout
import voluptuous as vol

from homeassistant.const import (
TEMP_CELSIUS,
CONF_IP_ADDRESS
)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.event import async_track_time_interval

_LOGGER = logging.getLogger(__name__)

# key: name of sensor
# value.0: index
# value.1: unit (String) or None
# from https://github.com/GitHobi/solax/wiki/direct-data-retrieval
INVERTER_SENSORS = {
'PV1 Current': (0, 'A'),
'PV2 Current': (1, 'A'),
'PV1 Voltage': (2, 'V'),
'PV2 Voltage': (3, 'V'),

'Output Current': (4, 'A'),
'Network Voltage': (5, 'V'),
'Power Now': (6, 'W'),

'Inverter Temperature': (7, TEMP_CELSIUS),
'Today\'s Energy': (8, 'kWh'),
'Total Energy': (9, 'kWh'),
'Exported Power': (10, 'W'),
'PV1 Power': (11, 'W'),
'PV2 Power': (12, 'W'),

'Battery Voltage': (13, 'V'),
'Battery Current': (14, 'A'),
'Battery Power': (15, 'W'),
'Battery Temperature': (16, TEMP_CELSIUS),
'Battery Remaining Capacity': (17, '%'),

'Battery Energy': (19, 'kWh'),

'Grid Frequency': (50, 'Hz'),
'EPS Voltage': (53, 'V'),
'EPS Current': (54, 'A'),
'EPS Power': (55, 'W'),
'EPS Frequency': (56, 'Hz'),
}

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_IP_ADDRESS): cv.string,
})

SCAN_INTERVAL = timedelta(seconds=30)
REQUEST_TIMEOUT = 5

REAL_TIME_DATA_ENDPOINT = 'http://{ip_address}/api/realTimeData.htm'

DATA_SCHEMA = vol.Schema(
vol.All([vol.Coerce(float)], vol.Length(min=68, max=68))
)

REAL_TIME_DATA_SCHEMA = vol.Schema({
vol.Required('method'): cv.string,
vol.Required('version'): cv.string,
vol.Required('type'): cv.string,
vol.Required('SN'): cv.string,
vol.Required('Data'): DATA_SCHEMA,
vol.Required('Status'): cv.positive_int,
}, extra=vol.REMOVE_EXTRA)


class SolaxRequestError(Exception):
"""Error to indicate a Solax API request has failed."""

pass


async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Platform setup."""
endpoint = RealTimeDataEndpoint(hass, config.get(CONF_IP_ADDRESS))
hass.async_add_job(endpoint.async_refresh)
async_track_time_interval(hass, endpoint.async_refresh, SCAN_INTERVAL)
devices = []
for sensor in INVERTER_SENSORS:
devices.append(Inverter(sensor))
endpoint.sensors = devices
async_add_entities(devices)


async def async_solax_real_time_request(hass, schema, ip_address, retry,
rohankapoorcom marked this conversation as resolved.
Show resolved Hide resolved
t_wait=0):
"""Make call to inverter endpoint."""
if t_wait > 0:
msg = "Timeout connecting to Solax inverter, waiting %d to retry."
_LOGGER.error(msg, t_wait)
asyncio.sleep(t_wait)
new_wait = (t_wait*2)+5
retry = retry - 1
try:
session = async_get_clientsession(hass)

with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
url = REAL_TIME_DATA_ENDPOINT.format(ip_address=ip_address)
req = await session.get(url)
garbage = await req.read()
formatted = garbage.decode("utf-8")
formatted = formatted.replace(",,", ",0.0,").replace(",,", ",0.0,")
json_response = json.loads(formatted)
return schema(json_response)
except asyncio.TimeoutError:
if retry > 0:
return await async_solax_real_time_request(hass,
schema,
ip_address,
retry,
new_wait)
_LOGGER.error("Too many timeouts connecting to Solax.")
except (aiohttp.ClientError) as client_err:
_LOGGER.error("Could not connect to Solax API endpoint")
_LOGGER.error(client_err)
except ValueError:
_LOGGER.error("Received non-JSON data from Solax API endpoint")
except vol.Invalid as err:
_LOGGER.error("Received unexpected JSON from Solax"
" API endpoint: %s", err)
_LOGGER.error(json_response)
raise SolaxRequestError


def parse_solax_battery_response(response):
"""Manipulate the response from solax endpoint."""
data_list = response['Data']
result = {}
for name, index in INVERTER_SENSORS.items():
response_index = index[0]
result[name] = data_list[response_index]
return result


class RealTimeDataEndpoint:
"""Representation of a Sensor."""

def __init__(self, hass, ip_address):
"""Initialize the sensor."""
self.hass = hass
self.ip_address = ip_address
self.data = {}
self.ready = asyncio.Event()
self.sensors = []

async def async_refresh(self, now=None):
"""Fetch new state data for the sensor.

This is the only method that should fetch new data for Home Assistant.
"""
try:
resp = await async_solax_real_time_request(self.hass,
REAL_TIME_DATA_SCHEMA,
self.ip_address,
3)
self.data = parse_solax_battery_response(resp)
self.ready.set()
except SolaxRequestError:
if now is not None:
self.ready.clear()
else:
raise PlatformNotReady
for sensor in self.sensors:
if sensor.key in self.data:
sensor.value = self.data[sensor.key]
sensor.async_schedule_update_ha_state()
squishykid marked this conversation as resolved.
Show resolved Hide resolved


class Inverter(Entity):
"""Class for a sensor."""

def __init__(self, key):
"""Initialize an inverter sensor."""
self.key = key
self.value = None

@property
def state(self):
"""State of this inverter attribute."""
return self.value

@property
def name(self):
"""Name of this inverter attribute."""
return self.key

@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return INVERTER_SENSORS[self.key][1]

@property
def should_poll(self):
"""No polling needed."""
return False