diff --git a/custom_components/4heat/__init__.py b/custom_components/4heat/__init__.py index 1845b49..96f4733 100644 --- a/custom_components/4heat/__init__.py +++ b/custom_components/4heat/__init__.py @@ -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 @@ -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, } ) }, diff --git a/custom_components/4heat/config_flow.py b/custom_components/4heat/config_flow.py index ccc946a..8a8cd62 100644 --- a/custom_components/4heat/config_flow.py +++ b/custom_components/4heat/config_flow.py @@ -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", ] @@ -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.""" @@ -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" @@ -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 ) @@ -94,6 +100,7 @@ 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, }, ) @@ -101,13 +108,23 @@ async def async_step_user(self, user_input=None): 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), } ) diff --git a/custom_components/4heat/const.py b/custom_components/4heat/const.py index f68349d..4003fd4 100644 --- a/custom_components/4heat/const.py +++ b/custom_components/4heat/const.py @@ -1,5 +1,6 @@ """Constants for the 4Heat integration.""" from datetime import timedelta +from tkinter import NO from homeassistant.const import ( TEMP_CELSIUS, @@ -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' @@ -27,6 +35,7 @@ MODE_TYPE = "30001" ERROR_TYPE = "30002" +POWER_TYPE = "20364" SENSOR_TYPES = { "30001": ["State", None, ""], @@ -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, ""], @@ -56,7 +65,7 @@ "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, ""], @@ -64,7 +73,7 @@ "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, ""], @@ -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", } \ No newline at end of file diff --git a/custom_components/4heat/coordinator.py b/custom_components/4heat/coordinator.py index e4c3590..275e296 100644 --- a/custom_components/4heat/coordinator.py +++ b/custom_components/4heat/coordinator.py @@ -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__) @@ -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" @@ -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 @@ -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") @@ -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") @@ -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") diff --git a/custom_components/4heat/manifest.json b/custom_components/4heat/manifest.json index f850a09..cd7a921 100644 --- a/custom_components/4heat/manifest.json +++ b/custom_components/4heat/manifest.json @@ -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, diff --git a/custom_components/4heat/sensor.py b/custom_components/4heat/sensor.py index 5024871..14a1e7d 100644 --- a/custom_components/4heat/sensor.py +++ b/custom_components/4heat/sensor.py @@ -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 @@ -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: @@ -61,13 +58,17 @@ 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: @@ -75,6 +76,11 @@ def state(self): 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.""" @@ -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) diff --git a/custom_components/4heat/switch.py b/custom_components/4heat/switch.py index 919aea8..11931d4 100644 --- a/custom_components/4heat/switch.py +++ b/custom_components/4heat/switch.py @@ -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: @@ -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.""" @@ -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 diff --git a/custom_components/4heat/translations/en.json b/custom_components/4heat/translations/en.json index b6ff9d0..36df08f 100644 --- a/custom_components/4heat/translations/en.json +++ b/custom_components/4heat/translations/en.json @@ -5,7 +5,8 @@ "user": { "data": { "name": "Name of the device", - "host": "The ip address of this 4Heat device" + "host": "The ip address of this 4Heat device", + "mode": "Legacy command mode" } } },