Climate control using json commands for iZone Panasonic Controller

Hi Team,

I have been trying to debug this for days but still not entirely successful. I have an Air Conditioner controller that is able to take json post requests. I have read the manual and been able to write a short python script that can control the unit and read store the system information. Once I had this working I tried to integrate it with the Broadlink IR Climate Custom Component Broadlink IR Climate Component. I have it working to a point where I can select temp and mode then see that the selected settings in the home-assistant.log.

Unfortunately, the AC Controller (iZone) is not accepting the post request when it is inside the custom_component script like it does when I use exactly the same commands in the python test script.

Here is the test code that is working perfectly. I have never learnt Python so there are likely much better ways to achieve the same result but it does work:
# https://linuxconfig.org/how-to-parse-data-from-json-into-python
import json
import requests

# defining the endpoint  
onENDPOINT = "http://192.168.0.139/SystemON"        #{"SystemON":"x"} "on" "off"
SetPointENDPOINT = "http://192.168.0.139/UnitSetpoint"        #{"SystemON":"x"} "on" "off"
ModeENDPOINT = "http://192.168.0.139/SystemMODE"   # {"SystemMODE":"x"} "cool" "heat" "vent" "dry" "auto"
SettingsENDPOINT = "http://192.168.0.139/SystemSettings"

dataOn = {"SystemON":"off"}
dataTemp = {'UnitSetpoint':'23'}
dataMode = {"SystemMODE":"vent"}

# sending post request and saving response as response object 
r = requests.post(url = onENDPOINT, json= dataOn) 
r = requests.post(url = ModeENDPOINT, json= dataMode) 
r = requests.post(SetPointENDPOINT, json= dataTemp) 

print("Response:")
print(r.url)
print(r.status_code)

rx = requests.post(SettingsENDPOINT) 

#format AC response into new line for each parameter
loaded_json = json.loads(rx.text)
for x in loaded_json:
	print("%s: %s" % (x, loaded_json[x]))

# store each value into an object
class sysInfo(object):
def __init__(self, data):
    self.__dict__ = json.loads(rx.text)

acInfo = sysInfo(rx.text)

SystemOn = acInfo.SysOn
SetPoint = acInfo.Setpoint
FanSpeed = acInfo.SysFan

print(SetPoint)

And here is the panasonic.py code that is only 50% working but not actually changing the state of the AC Control: The code that i have been playing with is inside the "def send_izone(self): " function.

# https://linuxconfig.org/how-to-parse-data-from-json-into-python
import json
# importing the requests library 
import requests 

import asyncio
import logging
import binascii
import socket
import os.path
import voluptuous as vol
import homeassistant.helpers.config_validation as cv

from homeassistant.core import callback
from homeassistant.components.climate import (ClimateDevice, PLATFORM_SCHEMA, STATE_OFF, STATE_IDLE, STATE_HEAT, STATE_COOL, STATE_AUTO,
ATTR_OPERATION_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE)
from homeassistant.const import (ATTR_UNIT_OF_MEASUREMENT, ATTR_TEMPERATURE, CONF_NAME, CONF_HOST, CONF_TIMEOUT, CONF_CUSTOMIZE)
from homeassistant.helpers.event import (async_track_state_change)
from homeassistant.helpers.restore_state import RestoreEntity
from configparser import ConfigParser
from base64 import b64encode, b64decode

REQUIREMENTS = ['broadlink==0.9.0']

_LOGGER = logging.getLogger(__name__)

SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE | SUPPORT_FAN_MODE

#CONF_IRCODES_INI = 'ircodes_ini'
CONF_MIN_TEMP = 'min_temp'
CONF_MAX_TEMP = 'max_temp'
CONF_TARGET_TEMP = 'target_temp'
CONF_TARGET_TEMP_STEP = 'target_temp_step'
CONF_TEMP_SENSOR = 'temp_sensor'
CONF_OPERATIONS = 'operations'
CONF_FAN_MODES = 'fan_modes'
CONF_DEFAULT_OPERATION = 'default_operation'
CONF_DEFAULT_FAN_MODE = 'default_fan_mode'

CONF_DEFAULT_OPERATION_FROM_IDLE = 'default_operation_from_idle'

DEFAULT_NAME = 'Broadlink IR Climate'
DEFAULT_TIMEOUT = 10
DEFAULT_RETRY = 3
DEFAULT_MIN_TEMP = 16
DEFAULT_MAX_TEMP = 30
DEFAULT_TARGET_TEMP = 20
DEFAULT_TARGET_TEMP_STEP = 1
DEFAULT_OPERATION_LIST = [STATE_OFF, STATE_HEAT, STATE_COOL, STATE_AUTO]
DEFAULT_FAN_MODE_LIST = ['low', 'mid', 'high', 'auto']
DEFAULT_OPERATION = 'off'
DEFAULT_FAN_MODE = 'auto'

OnENDPOINT = "http://192.168.0.139/SystemON"        #{"SystemON":"x"} "on" "off"
SetPointENDPOINT = "http://192.168.0.139/UnitSetpoint"        #{"SystemON":"x"} "on" "off"
ModeENDPOINT = "http://192.168.0.139/SystemMODE"   # {"SystemMODE":"x"} "cool" "heat" "vent" "dry" "auto"
SettingsENDPOINT = "http://192.168.0.139/SystemSettings"

dataOn = {"SystemON":"on"}
dataTemp = {"UnitSetpoint":"23"}
dataMode = {"SystemMODE":"vent"}

CUSTOMIZE_SCHEMA = vol.Schema({
    vol.Optional(CONF_OPERATIONS): vol.All(cv.ensure_list, [cv.string]),
    vol.Optional(CONF_FAN_MODES): vol.All(cv.ensure_list, [cv.string])
})

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
    vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
    vol.Required(CONF_HOST): cv.string,
#    vol.Required(CONF_MAC): cv.string,
#    vol.Required(CONF_IRCODES_INI): cv.string,
    vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, 
    vol.Optional(CONF_MIN_TEMP, default=DEFAULT_MIN_TEMP): vol.Coerce(float),
    vol.Optional(CONF_MAX_TEMP, default=DEFAULT_MAX_TEMP): vol.Coerce(float),
    vol.Optional(CONF_TARGET_TEMP, default=DEFAULT_TARGET_TEMP): vol.Coerce(float),
    vol.Optional(CONF_TARGET_TEMP_STEP, default=DEFAULT_TARGET_TEMP_STEP): vol.Coerce(float),
    vol.Optional(CONF_TEMP_SENSOR): cv.entity_id,
    vol.Optional(CONF_CUSTOMIZE, default={}): CUSTOMIZE_SCHEMA,
    vol.Optional(CONF_DEFAULT_OPERATION, default=DEFAULT_OPERATION): cv.string,
    vol.Optional(CONF_DEFAULT_FAN_MODE, default=DEFAULT_FAN_MODE): cv.string,
    vol.Optional(CONF_DEFAULT_OPERATION_FROM_IDLE): cv.string
})

async def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
    """Set up the Broadlink IR Climate platform."""
    name = config.get(CONF_NAME)
    ip_addr = config.get(CONF_HOST)
#    mac_addr = binascii.unhexlify(config.get(CONF_MAC).encode().replace(b':', b''))
      
    min_temp = config.get(CONF_MIN_TEMP)
    max_temp = config.get(CONF_MAX_TEMP)
    target_temp = config.get(CONF_TARGET_TEMP)
    target_temp_step = config.get(CONF_TARGET_TEMP_STEP)
    temp_sensor_entity_id = config.get(CONF_TEMP_SENSOR)
    operation_list = config.get(CONF_CUSTOMIZE).get(CONF_OPERATIONS, []) or DEFAULT_OPERATION_LIST
    fan_list = config.get(CONF_CUSTOMIZE).get(CONF_FAN_MODES, []) or DEFAULT_FAN_MODE_LIST
    default_operation = config.get(CONF_DEFAULT_OPERATION)
    default_fan_mode = config.get(CONF_DEFAULT_FAN_MODE)
    
    default_operation_from_idle = config.get(CONF_DEFAULT_OPERATION_FROM_IDLE)
    
    async_add_devices([
        BroadlinkIRClimate(hass, name, min_temp, max_temp, target_temp, target_temp_step, temp_sensor_entity_id, operation_list, fan_list, default_operation, default_fan_mode, default_operation_from_idle)
    ])

class BroadlinkIRClimate(ClimateDevice, RestoreEntity):

    def __init__(self, hass, name, min_temp, max_temp, target_temp, target_temp_step, temp_sensor_entity_id, operation_list, fan_list, default_operation, default_fan_mode, default_operation_from_idle):
                 
        """Initialize the Broadlink IR Climate device."""
        self.hass = hass
        self._name = name

        self._min_temp = min_temp
        self._max_temp = max_temp
        self._target_temperature = target_temp
        self._target_temperature_step = target_temp_step
        self._unit_of_measurement = hass.config.units.temperature_unit
        
        self._current_temperature = 0
        self._temp_sensor_entity_id = temp_sensor_entity_id

        self._current_operation = default_operation
        self._current_fan_mode = default_fan_mode
        
        self._operation_list = operation_list
        self._fan_list = fan_list
        
        self._default_operation_from_idle = default_operation_from_idle
                       
        if temp_sensor_entity_id:
            async_track_state_change(
                hass, temp_sensor_entity_id, self._async_temp_sensor_changed)
                
            sensor_state = hass.states.get(temp_sensor_entity_id)    
                
            if sensor_state:
                self._async_update_current_temp(sensor_state)
    
    
    def send_izone(self):     
        _LOGGER.error("Send data to iZone")
        section = self._current_operation.lower()
        
        if section == 'idle':
#            value = 'off_command'
            jsonOn = {'SystemON':'off'}
            r = requests.post('http://192.168.0.139/SystemON', json = {'SystemON':'off'},timeout=1)

            _LOGGER.error("Sending command [%s] to %s", jsonOn, self._name)

        else: 
            jsonTemp = {'UnitSetpoint':'{0:g}'.format(float(self._target_temperature))}
            jsonMode = {'SystemMODE':self._current_fan_mode.lower()}
            jsonOn = {'SystemON':'on'}
            _LOGGER.error("Sending command [%s %s] to %s", jsonTemp, jsonMode, self._name)
            
#            r = requests.post('http://192.168.0.139/UnitSetpoint', json= {'UnitSetpoint':'27'}, timeout=1)
            r = requests.post(SetPointENDPOINT, json= jsonTemp,timeout=1)
            _LOGGER.error(r.url)
            _LOGGER.error(r.status_code)

            r = requests.post(url = ModeENDPOINT, json= jsonMode,timeout=1)
            _LOGGER.error(r.url)
            _LOGGER.error(r.status_code)
        
#        r = requests.post('http://192.168.0.139/SystemON', json = {'SystemON':'on'},timeout=1)
        r = requests.post(OnENDPOINT, json= jsonOn, timeout=1)
        _LOGGER.error(r.url)
        _LOGGER.error(r.status_code)


        rx = requests.post(SettingsENDPOINT) 
        _LOGGER.error(rx.url)

        #format AC response into new line for each parameter
        loaded_json = json.loads(rx.text)
        for x in loaded_json:
            _LOGGER.error("%s: %s" % (x, loaded_json[x]))

        # store each value into an object
        class sysInfo(object):
            def __init__(self, data):
                self.__dict__ = json.loads(rx.text)

        acInfo = sysInfo(rx.text)
        
        SystemOn = acInfo.SysOn
        SetPoint = acInfo.Setpoint
        FanSpeed = acInfo.SysFan
        Mode = acInfo.SysMode

        _LOGGER.error(SetPoint)
            
    async def _async_temp_sensor_changed(self, entity_id, old_state, new_state):
        """Handle temperature changes."""
        if new_state is None:
            return

        self._async_update_current_temp(new_state)
        await self.async_update_ha_state()
        
    @callback
    def _async_update_current_temp(self, state):
        """Update thermostat with latest state from sensor."""
        unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)

        try:
            _state = state.state
            if self.represents_float(_state):
                self._current_temperature = self.hass.config.units.temperature(
                    float(_state), unit)
        except ValueError as ex:
            _LOGGER.error('Unable to update from sensor: %s', ex)    

    def represents_float(self, s):
        try: 
            float(s)
            return True
        except ValueError:
            return False     

    
    @property
    def should_poll(self):
        """Return the polling state."""
        return False

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

    @property
    def temperature_unit(self):
        """Return the unit of measurement."""
        return self._unit_of_measurement

    @property
    def current_temperature(self):
        """Return the current temperature."""
        return self._current_temperature
        
    @property
    def min_temp(self):
        """Return the polling state."""
        return self._min_temp
        
    @property
    def max_temp(self):
        """Return the polling state."""
        return self._max_temp    
        
    @property
    def target_temperature(self):
        """Return the temperature we try to reach."""
        return self._target_temperature
        
    @property
    def target_temperature_step(self):
        """Return the supported step of target temperature."""
        return self._target_temperature_step

    @property
    def current_operation(self):
        """Return current operation ie. heat, cool, idle."""
        return self._current_operation

    @property
    def operation_list(self):
        """Return the list of available operation modes."""
        return self._operation_list

    @property
    def current_fan_mode(self):
        """Return the fan setting."""
        return self._current_fan_mode

    @property
    def fan_list(self):
        """Return the list of available fan modes."""
        return self._fan_list
        
    @property
    def supported_features(self):
        """Return the list of supported features."""
        return SUPPORT_FLAGS        
 
    def set_temperature(self, **kwargs):
        """Set new target temperatures."""
        if kwargs.get(ATTR_TEMPERATURE) is not None:
            self._target_temperature = kwargs.get(ATTR_TEMPERATURE)
            
            if not (self._current_operation.lower() == 'off' or self._current_operation.lower() == 'idle'):
                self.send_izone()
            elif self._default_operation_from_idle is not None:
                self.set_operation_mode(self._default_operation_from_idle)
                
                    
            self.schedule_update_ha_state()

    def set_fan_mode(self, fan):
        """Set new target temperature."""
        self._current_fan_mode = fan
        
        if not (self._current_operation.lower() == 'off' or self._current_operation.lower() == 'idle'):
            self.send_izone()
            
        self.schedule_update_ha_state()

    def set_operation_mode(self, operation_mode):
        """Set new target temperature."""
        self._current_operation = operation_mode

        self.send_izone()
        self.schedule_update_ha_state()
        
    async def async_added_to_hass(self):
        """Run when entity about to be added."""
        await super().async_added_to_hass()
    
        last_state = await self.async_get_last_state()
        
        if last_state is not None:
            self._target_temperature = last_state.attributes['temperature']
            self._current_operation = last_state.attributes['operation_mode']
            self._current_fan_mode = last_state.attributes['fan_mode']

Thanks in anticipation and hopefully this might also help someone else once it is solved!

Also: the lines 174 to 194 seem to be working perfectly and pull data from the AC controller. It is just getting the controller to accept the “r = requests.post(SetPointENDPOINT, json= jsonTemp,timeout=1)” that seems to be causing the bigest problem.

So I just tested the Test code using Python3 rather than Python and interestingly it did not work, and responded exactly the same as my modified custom component. So… I think i already knew HA is using Python3 but it did not occur to me that I was testing the test script with Python2.

Could anyone help me adjust the code so that it will work with Python3? I think that would solve my problem.

Thanks

Can’t help right now unfortunately, but I’ll be keeping a keen eye, this is exactly the setup I’ll have in the near-ish future. If you’re not done in 6 months I’ll have a go :slight_smile:

Edit: Rather than doing it all from scratch, have you tried using the IZone python module and just writing the middleware to HA?

Further edit: existing component by the same author:

Well done callifo! that did not exist when i started this project which is why I didn’t find it my self. I should have done some googling when I picked up the project again. I have just loading the custom component. No success yet but i will let you know if i get either version working.

Hi @callifo,

I tried Swamp iZone component but it did not work for my setup without having Python 3.6 or higher. Should work on HASS.IO but have not had an oppertunity to test.

Meanwhile the guys at iZone got back to me with some alternate code that worked well for me.

Certainly not the best solution but this is currently working well for me:

I would be happy for it to be developed further if you are keen.