Skip to content

Commit

Permalink
Added legacy command mode, discovered more sensors
Browse files Browse the repository at this point in the history
  • Loading branch information
zaubererty committed Feb 23, 2022
1 parent 31e8e4b commit f8d0ee9
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 47 deletions.
5 changes: 4 additions & 1 deletion custom_components/4heat/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
from homeassistant.const import (
CONF_HOST,
CONF_NAME,
CONF_MONITORED_CONDITIONS,
)
import homeassistant.helpers.config_validation as cv

from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.typing import HomeAssistantType

from .const import DOMAIN, DATA_COORDINATOR
from .const import DOMAIN, DATA_COORDINATOR, CONF_MODE
from .coordinator import FourHeatDataUpdateCoordinator


Expand All @@ -21,6 +22,8 @@
{
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_MODE, default=False): cv.boolean,
vol.Optional(CONF_MONITORED_CONDITIONS): cv.ensure_list,
}
)
},
Expand Down
29 changes: 23 additions & 6 deletions custom_components/4heat/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@
DATA_QUERY,
SOCKET_BUFFER,
SOCKET_TIMEOUT,
TCP_PORT
TCP_PORT,
CONF_MODE,
CMD_MODE_OPTIONS
)

SUPPORTED_SENSOR_TYPES = list(SENSOR_TYPES)

DEFAULT_MONITORED_CONDITIONS = [
"device",
"30001",
]


Expand All @@ -47,7 +49,7 @@ class FourHeatConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):

CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL

conditions=""
conditions=[]

def __init__(self) -> None:
"""Initialize the config flow."""
Expand All @@ -67,9 +69,12 @@ def _check_host(self, host) -> bool:
s.settimeout(SOCKET_TIMEOUT)
s.connect((host, TCP_PORT))
s.send(DATA_QUERY)
self.conditions = s.recv(SOCKET_BUFFER).decode()
result= s.recv(SOCKET_BUFFER).decode()
s.close()
if len(self.conditions) > 10:
result = result.replace("]","")
result = result.replace('"',"")
self.conditions = result.split(",")
if len(self.conditions) > 3:
return True
except (ConnectTimeout, HTTPError):
self._errors[CONF_HOST] = "could_not_connect"
Expand All @@ -86,6 +91,7 @@ async def async_step_user(self, user_input=None):
else:
name = user_input[CONF_NAME]
host = user_input[CONF_HOST]
legacy_cmd = user_input[CONF_MODE]
can_connect = await self.hass.async_add_executor_job(
self._check_host, host
)
Expand All @@ -94,20 +100,31 @@ async def async_step_user(self, user_input=None):
title=f"{name}",
data={
CONF_HOST: host,
CONF_MODE: legacy_cmd,
CONF_MONITORED_CONDITIONS: self.conditions,
},
)
else:
user_input = {}
user_input[CONF_NAME] = "Stove"
user_input[CONF_HOST] = "192.168.0.0"
user_input[CONF_MODE] = False

default_monitored_conditions = (
[] if self._async_current_entries() else DEFAULT_MONITORED_CONDITIONS
self.conditions if len(self.conditions) == 0 else DEFAULT_MONITORED_CONDITIONS
)

setup_schema = vol.Schema(
{
vol.Required(CONF_NAME, default=user_input[CONF_NAME]): str,
vol.Required(CONF_HOST, default=user_input[CONF_HOST]): str,
vol.Optional(
CONF_MODE, default=user_input[CONF_MODE],
description='mode'
): bool,
vol.Optional(
CONF_MONITORED_CONDITIONS, default=default_monitored_conditions
): cv.multi_select(self.conditions),
}
)

Expand Down
25 changes: 22 additions & 3 deletions custom_components/4heat/const.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Constants for the 4Heat integration."""
from datetime import timedelta
from tkinter import NO

from homeassistant.const import (
TEMP_CELSIUS,
Expand All @@ -13,6 +14,13 @@
OFF_CMD = b'["SEC","1","J30254000000000001"]' # OFF
ON_CMD = b'["SEC","1","J30253000000000001"]' # ON

OFF_CMD_OLD = b'["SEC","1","1"]' # OFF
ON_CMD_OLD = b'["SEC","1","0"]' # ON

MODES = [[ON_CMD, OFF_CMD, UNBLOCK_CMD], [ON_CMD_OLD, OFF_CMD_OLD, None]]
CONF_MODE = 'mode'
CMD_MODE_OPTIONS = ['Full set (default)', 'Limited set']

RESULT_VALS = 'SEC'
RESULT_ERROR = 'ERR'

Expand All @@ -27,6 +35,7 @@

MODE_TYPE = "30001"
ERROR_TYPE = "30002"
POWER_TYPE = "20364"

SENSOR_TYPES = {
"30001": ["State", None, ""],
Expand All @@ -44,7 +53,7 @@
"30017": ["Boiler water", TEMP_CELSIUS, ""],
"30020": ["UN 30020", None, ""],
"30025": ["Comb.FanRealSpeed", None, ""],
"30026": ["UN 30026", None, ""],
"30026": ["UN 30026", TEMP_CELSIUS, ""],
"30033": ["UN 30033", None, ""],
"30040": ["UN 30040", None, ""],
"30044": ["UN 30044", None, ""],
Expand All @@ -56,15 +65,15 @@
"20206": ["UN 20206", None, ""],
"20211": ["UN 20211", None, ""],
"20225": ["UN 20225", None, ""],
"20364": ["UN 20364", None, ""],
"20364": ["Power Setting", None, ""],
"20381": ["UN 20381", None, ""],
"20365": ["UN 20365", None, ""],
"20366": ["UN 20366", None, ""],
"20369": ["UN 20369", None, ""],
"20374": ["UN 20374", None, ""],
"20375": ["UN 20375", None, ""],
"20575": ["UN 20575", None, ""],
"20493": ["UN 20493", None, ""],
"20493": ["Room temperature set point", TEMP_CELSIUS, ""],
"20570": ["UN 20570", None, ""],
"20801": ["Heating power", None, ""],
"20803": ["UN 20803", None, ""],
Expand Down Expand Up @@ -114,4 +123,14 @@
16: "Ignition",
17: "Ignition",
18: "Lack of Voltage Supply",
}

POWER_NAMES = {
1: "P1",
2: "P2",
3: "P3",
4: "P4",
5: "P5",
6: "P6",
7: "Auto",
}
43 changes: 30 additions & 13 deletions custom_components/4heat/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .const import (
DOMAIN, SOCKET_BUFFER, SOCKET_TIMEOUT, TCP_PORT, DATA_QUERY, ERROR_QUERY,
ON_CMD, OFF_CMD, UNBLOCK_CMD, RESULT_VALS, RESULT_ERROR
DOMAIN, SOCKET_BUFFER, SOCKET_TIMEOUT, TCP_PORT, DATA_QUERY, ERROR_QUERY,
RESULT_ERROR, CONF_MODE, MODES, MODE_TYPE, ERROR_TYPE
)

_LOGGER = logging.getLogger(__name__)
Expand All @@ -23,6 +23,22 @@ class FourHeatDataUpdateCoordinator(DataUpdateCoordinator):
def __init__(self, hass: HomeAssistantType, *, config: dict, options: dict):
"""Initialize global 4heat data updater."""
self._host = config[CONF_HOST]
self._mode = False
self.swiches = [MODE_TYPE]

if CONF_MODE in config:
self._mode = config[CONF_MODE]

if self._mode == False:
self._on_cmd = MODES[0][0]
self._off_cmd = MODES[0][1]
self._unblock_cmd = MODES[0][2]
self.swiches.append(ERROR_TYPE)
else:
self._on_cmd = MODES[1][0]
self._off_cmd = MODES[1][1]
self._unblock_cmd = MODES[1][2]

self._next_update = 0
self.model = "Basic"
self.serial_number = "1"
Expand Down Expand Up @@ -61,18 +77,19 @@ def _update_data() -> dict:
dict = self.data
if dict == None:
dict = {}

if list[0] == RESULT_ERROR:
list = _query_stove(ERROR_QUERY)

for data in list:
if len(data) > 3:
dict[data[1:6]] = int(data[7:])
if len(list) > 0:
if list[0] == RESULT_ERROR:
list = _query_stove(ERROR_QUERY)
for data in list:
if len(data) > 3:
dict[data[1:6]] = [int(data[7:]), data[0]]
return dict

try:
async with timeout(10):
return await self.hass.async_add_executor_job(_update_data)
d = await self.hass.async_add_executor_job(_update_data)
return d
except Exception as error:
raise UpdateFailed(f"Invalid response from API: {error}") from error

Expand All @@ -81,7 +98,7 @@ async def async_turn_on(self) -> bool:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(SOCKET_TIMEOUT)
s.connect((self._host, TCP_PORT))
s.send(ON_CMD)
s.send(self._on_cmd)
s.recv(SOCKET_BUFFER).decode()
s.close()
_LOGGER.debug("Toggle ON")
Expand All @@ -93,7 +110,7 @@ async def async_turn_off(self) -> bool:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(SOCKET_TIMEOUT)
s.connect((self._host, TCP_PORT))
s.send(OFF_CMD)
s.send(self._off_cmd)
s.recv(SOCKET_BUFFER).decode()
s.close()
_LOGGER.debug("Toggle OFF")
Expand All @@ -105,7 +122,7 @@ async def async_unblock(self) -> bool:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(SOCKET_TIMEOUT)
s.connect((self._host, TCP_PORT))
s.send(UNBLOCK_CMD)
s.send(self._unblock_cmd)
s.recv(SOCKET_BUFFER).decode()
s.close()
_LOGGER.debug("Toggle Unblock")
Expand Down
2 changes: 1 addition & 1 deletion custom_components/4heat/manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"domain": "4heat",
"version": "0.0.2",
"version": "0.1.0",
"name": "4Heat Stove",
"documentation": "https://github.com/zaubererty/homeassistant-4heat",
"config_flow": true,
Expand Down
39 changes: 23 additions & 16 deletions custom_components/4heat/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import (
MODE_NAMES, ERROR_NAMES, SENSOR_TYPES, DOMAIN, DATA_COORDINATOR, MODE_TYPE, ERROR_TYPE
MODE_NAMES, ERROR_NAMES, POWER_NAMES,
MODE_TYPE, ERROR_TYPE, POWER_TYPE,
SENSOR_TYPES, DOMAIN, DATA_COORDINATOR
)
from .coordinator import FourHeatDataUpdateCoordinator

Expand All @@ -17,13 +19,8 @@ async def async_setup_entry(hass, entry, async_add_entities):
coordinator: FourHeatDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][
DATA_COORDINATOR
]

entities = []

result = entry.data[CONF_MONITORED_CONDITIONS]
result = result.replace("]","")
result = result.replace('"',"")
sensorIds = result.split(",")
sensorIds = entry.data[CONF_MONITORED_CONDITIONS]

for sensorId in sensorIds:
if len(sensorId) > 5:
Expand Down Expand Up @@ -61,20 +58,29 @@ def name(self):
@property
def state(self):
"""Return the state of the device."""
if self.type not in self.coordinator.data:
return None
try:
if self.type == MODE_TYPE:
state = MODE_NAMES[self.coordinator.data[self.type]]
state = MODE_NAMES[self.coordinator.data[self.type][0]]
elif self.type == ERROR_TYPE:
state = ERROR_NAMES[self.coordinator.data[self.type]]
state = ERROR_NAMES[self.coordinator.data[self.type][0]]
elif self.type == POWER_TYPE:
state = POWER_NAMES[self.coordinator.data[self.type][0]]
else:
state = self.coordinator.data[self.type]
state = self.coordinator.data[self.type][0]

self._last_value = state
except Exception as ex:
_LOGGER.error(ex)
state = self._last_value
return state

@property
def maker(self):
"""Maker information"""
return self.coordinator.data[self.type][1]

@property
def unit_of_measurement(self):
"""Return the unit of measurement this sensor expresses itself in."""
Expand Down Expand Up @@ -103,12 +109,13 @@ def device_info(self):
@property
def state_attributes(self):
try:
if self.type == MODE_TYPE:
return {"Num Val": self.coordinator.data[self.type]}
elif self.type == ERROR_TYPE:
return {"Num Val": self.coordinator.data[self.type]}
else:
return None
val = {"Marker": self.coordinator.data[self.type][1]}
val["Reading ID"] = self.type

if self.type == MODE_TYPE or self.type == ERROR_TYPE or self.type == POWER_TYPE:
val["Num Val"] = self.coordinator.data[self.type][0]

return val

except Exception as ex:
_LOGGER.error(ex)
Expand Down
14 changes: 8 additions & 6 deletions custom_components/4heat/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
]
entities = []

for sensorId in [MODE_TYPE, ERROR_TYPE]:
for sensorId in coordinator.swiches:
try:
entities.append(FourHeatSwitch(coordinator, sensorId, entry.title))
except:
Expand Down Expand Up @@ -51,10 +51,12 @@ def name(self):
@property
def is_on(self):
"""Return true if switch is on."""
if self.type not in self.coordinator.data:
return False
if self.type == MODE_TYPE:
return self.coordinator.data[self.type] not in [0,7,8,9]
return self.coordinator.data[self.type][0] not in [0,7,8,9]
elif self.type == ERROR_TYPE:
return self.coordinator.data[self.type] != 0
return self.coordinator.data[self.type][0] != 0

async def async_turn_on(self, **kwargs):
"""Turn the switch on."""
Expand Down Expand Up @@ -93,11 +95,11 @@ def state_attributes(self):
try:
if self.type == MODE_TYPE:
return {
"Num Val": self.coordinator.data[self.type],
"Val text": MODE_NAMES[self.coordinator.data[self.type]]
"Num Val": self.coordinator.data[self.type][0],
"Val text": MODE_NAMES[self.coordinator.data[self.type][0]]
}
elif self.type == ERROR_TYPE:
return {"Num Val": self.coordinator.data[self.type]}
return {"Num Val": self.coordinator.data[self.type][0]}
else:
return None

Expand Down
Loading

0 comments on commit f8d0ee9

Please sign in to comment.