Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
bj00rn committed Nov 28, 2022
1 parent fb2bae2 commit c1dbf96
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 46 deletions.
9 changes: 7 additions & 2 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
// See https://aka.ms/vscode-remote/devcontainer.json for format details.
{
"image": "ghcr.io/ludeeus/devcontainer/integration:stable",
"name": "SalerydLoke integration development",
"context": "..",
"appPort": [
"9123:8123"
],
"mounts": [
"type=bind,source=/etc/localtime,target=/etc/localtime,readonly"
],
"containerEnv": {
"TZ": "Europe/Stockholm"
},
"postCreateCommand": "container install",
"extensions": [
"ms-python.python",
Expand All @@ -27,4 +32,4 @@
"editor.formatOnType": true,
"files.trimTrailingWhitespace": true
}
}
}
4 changes: 2 additions & 2 deletions custom_components/saleryd_ftx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,15 +95,15 @@ class SalerydLokeDataUpdateCoordinator(DataUpdateCoordinator):

def __init__(self, hass: HomeAssistant, client: SalerydLokeApiClient) -> None:
"""Initialize."""
self.api = client
self._api = client
self.platforms = []

super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL)

async def _async_update_data(self):
"""Update data via library."""
try:
return await self.api.async_get_data()
return await self._api.async_get_data()
except Exception as exception:
raise UpdateFailed() from exception

Expand Down
91 changes: 60 additions & 31 deletions custom_components/saleryd_ftx/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import logging
import asyncio
import socket
from typing import Optional
from datetime import date, datetime
import aiohttp
import async_timeout
from homeassistant.exceptions import IntegrationError
Expand All @@ -23,13 +23,41 @@ def __init__(self, url, session: aiohttp.ClientSession) -> None:
"""Sample API Client."""
self._url = url
self._session = session
self._ws = None

async def init_ws(self):
"""Init websocket connection"""
async with async_timeout.timeout(TIMEOUT):
_LOGGER.debug("Connecting to websocket %s", self._url)
self._ws = await self._session.ws_connect(self._url)
_LOGGER.debug("Connnected to websocket %s", self._url)
# Set time on server and begin connection
command = f"#CS:{date.strftime(datetime.now(), '%y-%m-%d-%w-%H-%M-%S')}"
await self._ws.send_str(command)
# Server should begin sending messages. Make sure we receive at least one message
response = await self._ws.receive_str()
_LOGGER.debug("Received first message from websocket: %s", response)

async def async_get_data(self) -> dict:
"""Get data from the API."""
is_socket_open = True

if not self._ws:
_LOGGER.debug("Websocket needs setting up")
is_socket_open = False
elif self._ws.closed:
_LOGGER.debug("Websocket was closed. Needs setting up")
is_socket_open = False

if not is_socket_open:
await self.init_ws()

return await self.api_wrapper("ws_get", self._url)

async def async_send_command(self, data):
return await self.api_wrapper("ws_set", self._url, data)
"""Send data to API"""
_LOGGER.debug("Outgoing command %s", data)
return await self.api_wrapper("ws_set", data)

def _parse_message(self, msg):
"""parse socket message"""
Expand All @@ -38,12 +66,14 @@ def _parse_message(self, msg):
try:
if msg[0] == "#":
if msg[1] == "?" or msg[1] == "$":
# ignore acks
_LOGGER.debug("Ignoring message %s", msg)
# ignore all acks end ack errors for now
_LOGGER.debug("Ignoring ack message %s", msg)
return

value = msg[1::].split(":")[1].strip()
if msg[1] != "*":
# messages beginning with * are arrays
# [value, min, max] or [value, min, max, time_left]
value = value.split("+")
key = msg[1::].split(":")[0]
parsed = (key, value)
Expand All @@ -52,64 +82,63 @@ def _parse_message(self, msg):
raise ParseError() from exc
return parsed

async def api_wrapper(
self, method: str, url: str, data: str = dict, headers: dict = dict
) -> dict:
async def api_wrapper(self, method, data: str = dict) -> dict:
"""Get information from the API."""

try:
async with async_timeout.timeout(TIMEOUT):
if method == "ws_get":
state = {}
async with self._session.ws_connect(self._url) as websocket:
_LOGGER.debug("Connected to %s", url)
command = "#\r"
_LOGGER.debug("Outgoing message %s", command)
await websocket.send_str(command)
nsamples = 100
count = 0
_LOGGER.debug("Starting sampling of %d messages", nsamples)
async for msg in websocket:
try:
_LOGGER.debug("Incoming message [%d]: %s", count, msg)
parsed = self._parse_message(msg.data)
key, value = parsed
state[key] = value
except ParseError:
pass
count = count + 1
if count >= nsamples:
state = dict()
count = 0
_LOGGER.debug("Starting collection of messages from server")
while True:
msg = await self._ws.receive_str()
try:
_LOGGER.debug("Incoming message [%d]: %s", count, msg)
parsed = self._parse_message(msg)
key, value = parsed
if state.get(key) and key != "*CV":
# We are done
_LOGGER.debug(
"Finished sampling, got %d messages", count
)
_LOGGER.debug("Got state %s", state)
return state
state[key] = value
except ParseError:
pass
count = count + 1
elif method == "ws_set":
async with self._session.ws_connect(self._url) as websocket:
await websocket.send_str(f"{data}\r")
await self._ws.send_str(f"{data}\r")

except asyncio.TimeoutError as exception:
_LOGGER.error(
"Timeout error fetching information from %s - %s",
url,
self._url,
exception,
)
raise exception

except (KeyError, TypeError) as exception:
_LOGGER.error(
"Error parsing information from %s - %s",
url,
self._url,
exception,
)
raise exception
except (aiohttp.ClientError, socket.gaierror) as exception:
_LOGGER.error(
"Error fetching information from %s - %s",
url,
self._url,
exception,
)
raise exception
except Exception as exception: # pylint: disable=broad-except
_LOGGER.error("Something really wrong happened! - %s", exception)
raise exception


class ParseError(IntegrationError):
"""Error when parsing websocket message fails"""

pass
51 changes: 42 additions & 9 deletions custom_components/saleryd_ftx/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,22 @@
)

from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.const import PERCENTAGE, TEMP_CELSIUS
from homeassistant.const import PERCENTAGE, TEMP_CELSIUS, UnitOfPower

from .const import DEFAULT_NAME, DOMAIN, ICON, SENSOR, ATTRIBUTION
from .const import DEFAULT_NAME, DOMAIN, ATTRIBUTION

import decimal

sensors = {
"heat_exchanger_rpm": SensorEntityDescription(
key="*XB",
name="Heat exchanger speed",
name="Heat exchanger rotor speed",
native_unit_of_measurement="rpm",
state_class=SensorStateClass.MEASUREMENT,
),
"heat_exchanger_speed": SensorEntityDescription(
key="*XB",
name="Heat exchanger speed percent",
name="Heat exchanger rotor speed percent",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
Expand All @@ -46,6 +46,20 @@
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=TEMP_CELSIUS,
),
"heater_temperature_percent": SensorEntityDescription(
key="*MJ",
name="Heater temperature percent",
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
),
"heater_power": SensorEntityDescription(
key="MG",
name="Heater power",
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfPower.WATT,
),
"target_temperature": SensorEntityDescription(
key="*DT",
name="Target temperature",
Expand Down Expand Up @@ -80,11 +94,17 @@
state_class=SensorStateClass.MEASUREMENT,
),
"temperature_mode": SensorEntityDescription(
key="MH",
key="MT",
icon="mdi:home-thermometer",
name="Temperature mode",
state_class=SensorStateClass.MEASUREMENT,
),
"filter_months_left": SensorEntityDescription(
key="FL",
icon="mdi:home-thermometer",
name="Filter months left",
state_class=SensorStateClass.MEASUREMENT,
),
}


Expand All @@ -110,30 +130,43 @@ def __init__(
coordinator: DataUpdateCoordinator,
entry_id,
entity_description: SensorEntityDescription,
data_type: type = decimal.Decimal,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)

self.entity_description = entity_description
self._id = entry_id
self._data_type = data_type

self._attr_name = entity_description.name
self._attr_unique_id = f"{entry_id}_{entity_description.key}"
self._attr_unique_id = f"{entry_id}_{entity_description.name}"
self._id = entry_id

self._attr_device_info = DeviceInfo(
configuration_url="https://dashboard.airthings.com/",
identifiers={(DOMAIN, entry_id)},
name=DEFAULT_NAME,
manufacturer="Airthings",
manufacturer="Saleryd",
)

def _translate_value(self, value):
if self.entity_description.key == "MG":
if value == 0:
return 900
elif value == 1:
return 1800
else:
return None

return value

@property
def native_value(self):
"""Return the native value of the sensor."""
value = self.coordinator.data.get(self.entity_description.key)
if value:
return decimal.Decimal(value[0] if isinstance(value, list) else value)
value = self._data_type(value[0] if isinstance(value, list) else value)
return self._translate_value(value)

@property
def extra_state_attributes(self):
Expand Down
3 changes: 2 additions & 1 deletion requirements_test.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pytest-homeassistant-custom-component==0.4.0
pytest-homeassistant-custom-component
pytest
2 changes: 1 addition & 1 deletion tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import aiohttp
from homeassistant.helpers.aiohttp_client import async_get_clientsession

from custom_components.integration_blueprint.api import SalerydLokeApiClient
from custom_components.saleryd_ftx.api import SalerydLokeApiClient


async def test_api(hass, aioclient_mock, caplog):
Expand Down

0 comments on commit c1dbf96

Please sign in to comment.