├── .gitignore ├── LICENSE ├── README.md ├── custom_components └── hisense │ ├── __init__.py │ ├── button.py │ ├── climate.py │ ├── config_flow.py │ ├── const.py │ ├── manifest.json │ ├── pyhisenseapi.py │ ├── switch.py │ └── translations │ ├── en.json │ └── zh-Hans.json └── hacs.json /.gitignore: -------------------------------------------------------------------------------- 1 | test.py 2 | __pycache__ 3 | vscode/* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Jiaxin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HisenseHA 2 | Home Assistant integration for Hisense smart device 3 | 4 | Only support AC currently. 5 | 6 | ## Release 7 | ### V1.3.2 8 | * Use https, see this [PR](https://github.com/manymuch/HisenseHA/pull/8) 9 | 10 | ### V1.3.1 11 | * Upgrade to Home Assistant 2025.6.1 12 | 13 | ### V1.3.0 14 | * Support multiple AC 15 | 16 | ### V1.2.0 17 | * Support login via username and password 18 | -------------------------------------------------------------------------------- /custom_components/hisense/__init__.py: -------------------------------------------------------------------------------- 1 | from homeassistant import config_entries, core 2 | from homeassistant.helpers.aiohttp_client import async_get_clientsession 3 | from .const import DOMAIN 4 | from .pyhisenseapi import HiSenseAC 5 | 6 | 7 | async def async_setup_entry(hass: core.HomeAssistant, entry: config_entries.ConfigEntry): 8 | hass.data.setdefault(DOMAIN, {}) 9 | hass.data[DOMAIN][entry.entry_id] = {} 10 | 11 | session = async_get_clientsession(hass) 12 | # Setup devices based on the selected devices from the config flow 13 | for device_info in entry.data["devices"]: 14 | device_id = device_info["device_id"] 15 | wifi_id = device_info["wifi_id"] 16 | refresh_token = device_info["refresh_token"] 17 | hass.data[DOMAIN][entry.entry_id][device_id] = HiSenseAC( 18 | wifi_id=wifi_id, 19 | device_id=device_id, 20 | refresh_token=refresh_token, 21 | session=session 22 | ) 23 | 24 | # Load platforms 25 | await hass.config_entries.async_forward_entry_setups(entry, ["climate", "switch", "button"]) 26 | return True 27 | 28 | async def async_unload_entry(hass: core.HomeAssistant, entry: config_entries.ConfigEntry): 29 | return await hass.config_entries.async_unload_platforms(entry, ["climate", "switch", "button"]) -------------------------------------------------------------------------------- /custom_components/hisense/button.py: -------------------------------------------------------------------------------- 1 | from homeassistant.components.button import ButtonEntity 2 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 3 | from homeassistant.core import HomeAssistant 4 | from .const import DOMAIN 5 | from homeassistant.const import EntityCategory 6 | import logging 7 | 8 | _LOGGER = logging.getLogger(__name__) 9 | 10 | 11 | 12 | async def async_setup_entry(hass, config_entry, async_add_entities): 13 | api = hass.data[DOMAIN][config_entry.entry_id] 14 | entities = [HisenseACUpdateButton(api[device_id], config_entry.entry_id) for device_id in api] 15 | async_add_entities(entities, True) 16 | entities = [HisenseACRefreshTokenButton(api[device_id], config_entry.entry_id) for device_id in api] 17 | async_add_entities(entities, True) 18 | 19 | 20 | class HisenseACUpdateButton(ButtonEntity): 21 | def __init__(self, api, config_entry_id): 22 | self._api = api 23 | self._config_entry_id = config_entry_id 24 | self._attr_name = f"Force update button" 25 | self._attr_unique_id = f"{api.device_id}_force_update_button" 26 | self._attr_icon = "mdi:refresh" 27 | 28 | @property 29 | def entity_category(self): 30 | return EntityCategory.CONFIG 31 | 32 | @property 33 | def device_info(self): 34 | return { 35 | "identifiers": {(DOMAIN, self._api.device_id)}, 36 | "name": "Hisense AC", 37 | "manufacturer": "Hisense", 38 | } 39 | 40 | @property 41 | def name(self): 42 | return "Force Update" 43 | 44 | async def async_press(self): 45 | """Handle the button press.""" 46 | _LOGGER.debug(f"Button pressed for entity: {self._attr_unique_id}") 47 | await self._api.check_status() 48 | self.async_schedule_update_ha_state(True) 49 | 50 | 51 | class HisenseACRefreshTokenButton(ButtonEntity): 52 | def __init__(self, api, config_entry_id): 53 | self._api = api 54 | self._config_entry_id = config_entry_id 55 | self._attr_name = f"Refresh token" 56 | self._attr_unique_id = f"{api.device_id}_refresh_token" 57 | self._attr_icon = "mdi:refresh" 58 | 59 | @property 60 | def entity_category(self): 61 | return EntityCategory.CONFIG 62 | 63 | @property 64 | def device_info(self): 65 | return { 66 | "identifiers": {(DOMAIN, self._api.device_id)}, 67 | "name": "Hisense AC", 68 | "manufacturer": "Hisense", 69 | } 70 | 71 | @property 72 | def name(self): 73 | return "Refresh token" 74 | 75 | async def async_press(self): 76 | """Handle the button press.""" 77 | _LOGGER.debug(f"Button pressed for entity: {self._attr_unique_id}") 78 | await self._api.refresh() 79 | -------------------------------------------------------------------------------- /custom_components/hisense/climate.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | from homeassistant.components.climate import ClimateEntity 4 | from homeassistant.components.climate.const import ( 5 | ClimateEntityFeature, 6 | HVACMode, 7 | FAN_AUTO, 8 | FAN_LOW, 9 | FAN_MEDIUM, 10 | FAN_HIGH, 11 | FAN_DIFFUSE, 12 | SWING_ON, 13 | SWING_OFF, 14 | SWING_HORIZONTAL, 15 | SWING_VERTICAL 16 | ) 17 | from homeassistant.const import UnitOfTemperature, ATTR_TEMPERATURE 18 | from .const import DOMAIN 19 | 20 | _LOGGER = logging.getLogger(__name__) 21 | 22 | 23 | async def async_setup_entry(hass, config_entry, async_add_entities): 24 | api = hass.data[DOMAIN][config_entry.entry_id] 25 | entities = [HisenseACClimate(api[device_id], config_entry.entry_id) for device_id in api] 26 | async_add_entities(entities, True) 27 | 28 | 29 | class HisenseACClimate(ClimateEntity): 30 | def __init__(self, api, config_entry_id): 31 | self._api = api 32 | self._config_entry_id = config_entry_id 33 | self._attr_name = f"Hisense AC" 34 | self._attr_unique_id = f"{api.device_id}_climate" 35 | self._attr_supported_features = ( 36 | ClimateEntityFeature.TURN_ON | 37 | ClimateEntityFeature.TURN_OFF | 38 | ClimateEntityFeature.TARGET_TEMPERATURE | 39 | ClimateEntityFeature.FAN_MODE | 40 | ClimateEntityFeature.SWING_MODE 41 | ) 42 | self._enable_turn_on_off_backwards_compatibility = False 43 | self._attr_fan_modes = [FAN_AUTO, FAN_DIFFUSE, 44 | FAN_LOW, FAN_MEDIUM, FAN_HIGH] 45 | self._attr_hvac_modes = [ 46 | HVACMode.COOL, HVACMode.HEAT, HVACMode.DRY, HVACMode.FAN_ONLY, HVACMode.OFF] 47 | self._attr_swing_modes = [ 48 | SWING_ON, SWING_OFF, SWING_HORIZONTAL, SWING_VERTICAL] 49 | self._attr_swing_mode = SWING_OFF 50 | self._attr_temperature_unit = UnitOfTemperature.CELSIUS 51 | self._attr_target_temperature = None 52 | self._attr_current_temperature = None 53 | self._attr_min_temp = 16 54 | self._attr_max_temp = 32 55 | self._attr_is_on = False 56 | self._attr_hvac_mode = None 57 | self._attr_fan_mode = None 58 | self._hvac_mode_lookup = { 59 | 0: HVACMode.FAN_ONLY, 60 | 1: HVACMode.HEAT, 61 | 2: HVACMode.COOL, 62 | 3: HVACMode.DRY, 63 | 4: HVACMode.AUTO, 64 | } 65 | self._hvac_mode_to_id = { 66 | HVACMode.FAN_ONLY: 0, 67 | HVACMode.HEAT: 1, 68 | HVACMode.COOL: 2, 69 | HVACMode.DRY: 3, 70 | HVACMode.AUTO: 4, 71 | } 72 | self._fan_mode_lookup = { 73 | 0: FAN_AUTO, 74 | 1: FAN_DIFFUSE, 75 | 2: FAN_LOW, 76 | 3: FAN_MEDIUM, 77 | 4: FAN_HIGH 78 | } 79 | self._fan_mode_to_id = { 80 | FAN_AUTO: 0, 81 | FAN_DIFFUSE: 1, 82 | FAN_LOW: 2, 83 | FAN_MEDIUM: 3, 84 | FAN_HIGH: 4 85 | } 86 | self._swing_mode_lookup = { 87 | 0: SWING_OFF, 88 | 1: SWING_ON, 89 | 2: SWING_HORIZONTAL, 90 | 3: SWING_VERTICAL 91 | } 92 | self._swing_mode_to_id = { 93 | SWING_OFF: 0, 94 | SWING_ON: 1, 95 | SWING_HORIZONTAL: 2, 96 | SWING_VERTICAL: 3 97 | } 98 | 99 | @property 100 | def device_info(self): 101 | return { 102 | "identifiers": {(DOMAIN, self._api.device_id)}, 103 | "name": "Hisense AC", 104 | "manufacturer": "Hisense", 105 | } 106 | 107 | async def async_update(self): 108 | status = self._api.get_status() 109 | self._attr_is_on = status.get("power_on") 110 | self._attr_current_temperature = status.get("indoor_temperature") 111 | self._attr_target_temperature = status.get("desired_temperature") 112 | if self._attr_is_on: 113 | self._attr_hvac_mode = self._hvac_mode_lookup[status.get( 114 | "hvac_mode_id", 4)] 115 | else: 116 | self._attr_hvac_mode = HVACMode.OFF 117 | self._attr_fan_mode = self._fan_mode_lookup[status.get( 118 | "fan_mode_id", 0)] 119 | self._attr_swing_mode = self._swing_mode_lookup[status.get( 120 | "swing_mode_id", 0)] 121 | 122 | async def async_set_temperature(self, **kwargs): 123 | """Set new target temperature.""" 124 | if not self._api.get_status().get("power_on"): 125 | _LOGGER.error("Cannot set temperature when power is off") 126 | return 127 | if self._attr_hvac_mode == HVACMode.FAN_ONLY: 128 | _LOGGER.error("Cannot set temperature in 'Fan Only' mode") 129 | return 130 | 131 | temperature = kwargs.get(ATTR_TEMPERATURE) 132 | if temperature is not None: 133 | # Convert temperature to Celsius if necessary 134 | unit = self.hass.config.units.temperature_unit 135 | if unit == UnitOfTemperature.FAHRENHEIT: 136 | temperature = temperature * 9 / 5 + 32 137 | # Now, temperature is guaranteed to be in Celsius 138 | await self._api.send_logic_command(6, int(temperature)) 139 | await self.async_update() 140 | 141 | async def async_set_fan_mode(self, fan_mode): 142 | """Set new target fan mode.""" 143 | if not self._api.get_status().get("power_on"): 144 | _LOGGER.error("Cannot set fan mode when power is off") 145 | return 146 | fan_id = self._fan_mode_to_id.get(fan_mode) 147 | await self._api.send_logic_command(1, fan_id) 148 | self._attr_fan_mode = fan_mode 149 | await self.async_update() 150 | 151 | async def async_set_swing_mode(self, swing_mode): 152 | """Set new target swing mode.""" 153 | if not self._api.get_status().get("power_on"): 154 | _LOGGER.error("Cannot set swing mode when power is off") 155 | return 156 | if swing_mode in self._attr_swing_modes: 157 | swing_id = self._swing_mode_to_id.get(swing_mode) 158 | await self._api.send_logic_command(62, swing_id) 159 | # Update the entity's current swing mode 160 | self._attr_swing_mode = swing_mode 161 | # Notify Home Assistant that the entity's state has changed 162 | self.async_schedule_update_ha_state(True) 163 | else: 164 | _LOGGER.error("Unsupported swing mode: %s", swing_mode) 165 | 166 | async def async_set_hvac_mode(self, hvac_mode): 167 | """Set new target HVAC mode.""" 168 | if hvac_mode == HVACMode.OFF: 169 | await self.async_turn_off() 170 | return 171 | 172 | hvac_id = self._hvac_mode_to_id.get(hvac_mode) 173 | power_on = self._api.get_status().get("power_on", False) 174 | same_hvac = self._api.get_status().get("hvac_mode_id") == hvac_id 175 | 176 | # power on same hvac -> set hvac (do nothing) 177 | # power on different hvac -> set hvac 178 | # power off same hvac -> turn on 179 | # power off different hvac -> turn on and set hvac 180 | if power_on: 181 | await self._api.send_logic_command(3, hvac_id) 182 | self._attr_hvac_mode = hvac_mode 183 | elif same_hvac: 184 | await self.async_turn_on() 185 | else: 186 | await self.async_turn_on() 187 | await asyncio.sleep(4) 188 | await self._api.send_logic_command(3, hvac_id) 189 | self._attr_hvac_mode = hvac_mode 190 | await self.async_update() 191 | 192 | async def async_turn_on(self): 193 | await self._api.turn_on() 194 | self._attr_is_on = True 195 | await self.async_update() 196 | 197 | async def async_turn_off(self): 198 | await self._api.turn_off() 199 | self._attr_hvac_mode = HVACMode.OFF 200 | self._attr_is_on = False 201 | await self.async_update() 202 | -------------------------------------------------------------------------------- /custom_components/hisense/config_flow.py: -------------------------------------------------------------------------------- 1 | import voluptuous as vol 2 | from homeassistant import config_entries 3 | from homeassistant.helpers.aiohttp_client import async_get_clientsession 4 | from homeassistant.helpers import config_validation as cv 5 | from .const import DOMAIN, CONF_USERNAME, CONF_PASSWORD 6 | from .pyhisenseapi import HiSenseLogin 7 | 8 | class HisenseACConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 9 | VERSION = 1 10 | CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL 11 | 12 | def __init__(self): 13 | self._home_id_list = None 14 | self._access_token = None 15 | self._refresh_token = None 16 | 17 | async def async_step_user(self, user_input=None): 18 | errors = {} 19 | 20 | if user_input is not None: 21 | session = async_get_clientsession(self.hass) 22 | hisense_login = HiSenseLogin(session=session) 23 | 24 | try: 25 | access_token, refresh_token = await hisense_login.login( 26 | user_input[CONF_USERNAME], user_input[CONF_PASSWORD] 27 | ) 28 | self._home_id_list = await hisense_login.get_home_id_list(access_token) 29 | self._access_token = access_token 30 | self._refresh_token = refresh_token 31 | return await self.async_step_home() 32 | except Exception: 33 | errors["base"] = "invalid_auth" 34 | 35 | return self.async_show_form( 36 | step_id="user", 37 | data_schema=vol.Schema( 38 | { 39 | vol.Required(CONF_USERNAME): str, 40 | vol.Required(CONF_PASSWORD): str, 41 | } 42 | ), 43 | description_placeholders={ 44 | "username_hint": "app login username", 45 | "password_hint": "app login password", 46 | }, 47 | errors=errors, 48 | ) 49 | 50 | async def async_step_home(self, user_input=None): 51 | errors = {} 52 | 53 | if user_input is not None: 54 | home_id = user_input["home_id"] 55 | session = async_get_clientsession(self.hass) 56 | hisense_login = HiSenseLogin(session=session) 57 | self._device_wifi_id_dict = await hisense_login.get_device_wifi_id_dict( 58 | self._access_token, home_id 59 | ) 60 | 61 | return await self.async_step_device() 62 | 63 | return self.async_show_form( 64 | step_id="home", 65 | data_schema=vol.Schema( 66 | {vol.Required("home_id"): vol.In(self._home_id_list)} 67 | ), 68 | errors=errors, 69 | ) 70 | 71 | async def async_step_device(self, user_input=None): 72 | if user_input is not None: 73 | device_ids = user_input["device_ids"] 74 | devices = [ 75 | {"device_id": device_id, 76 | "wifi_id": self._device_wifi_id_dict[device_id], 77 | "refresh_token": self._refresh_token, 78 | } 79 | for device_id in device_ids 80 | ] 81 | return self.async_create_entry( 82 | title="Hisense Smart Control", 83 | data={"devices": devices} 84 | ) 85 | 86 | data_schema = vol.Schema( 87 | { 88 | vol.Required("device_ids"): cv.multi_select( 89 | list(self._device_wifi_id_dict.keys()) 90 | ), 91 | } 92 | ) 93 | return self.async_show_form(step_id="device", data_schema=data_schema) 94 | -------------------------------------------------------------------------------- /custom_components/hisense/const.py: -------------------------------------------------------------------------------- 1 | """Constants for HiSense Integration.""" 2 | DOMAIN = "hisense" 3 | 4 | # Configuration keys 5 | CONF_USERNAME = "username" 6 | CONF_PASSWORD = "password" 7 | -------------------------------------------------------------------------------- /custom_components/hisense/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "hisense", 3 | "name": "Hisense Smart Devices", 4 | "config_flow": true, 5 | "version": "1.3.2", 6 | "integration_type": "device", 7 | "documentation": "https://github.com/manymuch/HisenseHA", 8 | "requirements": ["asyncio"], 9 | "iot_class": "cloud_polling", 10 | "codeowners": [ 11 | "@manymuch" 12 | ] 13 | } -------------------------------------------------------------------------------- /custom_components/hisense/pyhisenseapi.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | import time 3 | import logging 4 | _LOGGER = logging.getLogger(__name__) 5 | 6 | 7 | class HiSenseLogin: 8 | def __init__(self, session): 9 | self.session = session 10 | 11 | def get_timestamp(self): 12 | return int(time.time() * 1000) 13 | 14 | async def login(self, username, password): 15 | timestamp = self.get_timestamp() 16 | url='https://portal-account.hismarttv.com/mobile/signon' 17 | headers = { 18 | 'Content-Type': 'application/json;charset=utf-8', 19 | } 20 | data = { 21 | 'pdateTime': '0', 22 | 'version': '1.0', 23 | 'deviceType': '2', 24 | 'appType': '100', 25 | 'versionCode': '101', 26 | 'adaptertRank': '3098', 27 | 'deviceType':'1', 28 | 'distributeId':'2001', 29 | 'loginName': username, 30 | 'serverCode':'9501', 31 | 'signature': password, 32 | } 33 | params = { 34 | 'lastUpdateTime': '0', 35 | 'version': '1.0', 36 | 'deviceType': '2', 37 | 'appType': '100', 38 | 'versionCode': '101', 39 | 'adaptertRank': '4130', 40 | '_': str(timestamp), 41 | } 42 | async with self.session.post(url, headers=headers, json=data, params=params) as response: 43 | result = await response.json() 44 | result_code = result["data"]["resultCode"] 45 | if result_code == 0: 46 | access_token = result["data"]["tokenInfo"]["token"] 47 | refresh_token = result["data"]["tokenInfo"]["refreshToken"] 48 | return access_token, refresh_token 49 | else: 50 | return None 51 | 52 | async def get_home_id_list(self, access_token): 53 | timestamp = self.get_timestamp() 54 | url='https://api-wg.hismarttv.com/wg/dm/getHomeList' 55 | 56 | headers = { 57 | 'Host': 'api.wg.hismarttv.com', 58 | 'Connection': 'Keep-Alive', 59 | 'Accept-Encoding': 'gzip', 60 | 'User-Agent': 'okhttp/4.10.0', 61 | } 62 | params = { 63 | 'sign': '', 64 | 'languageId': '0', 65 | 'version': '8.0', 66 | 'accessToken': access_token, 67 | 'timezone':'28800', 68 | 'format': '1', 69 | 'timeStamp': str(timestamp), 70 | } 71 | async with self.session.get(url, headers=headers, params=params) as response: 72 | result = await response.json() 73 | result_code = result["response"]["resultCode"] 74 | if result_code == 0: 75 | home_list = result["response"]["homeList"] 76 | home_id_list = [] 77 | for home in home_list: 78 | home_id_list.append(home["homeId"]) 79 | return home_id_list 80 | else: 81 | return None 82 | 83 | async def get_device_wifi_id_dict(self, access_token, home_id, device_keywords="空调"): 84 | timestamp = self.get_timestamp() 85 | url='https://api-wg.hismarttv.com/wg/dm/getHomeDeviceList' 86 | headers = { 87 | 'Host': 'api-wg.hismarttv.com', 88 | 'Connection': 'Keep-Alive', 89 | 'Accept-Encoding': 'gzip', 90 | 'User-Agent': 'okhttp/4.10.0', 91 | } 92 | params = { 93 | 'sign': '', 94 | 'languageId': '0', 95 | 'version': '8.0', 96 | 'accessToken': access_token, 97 | 'homeId': home_id, 98 | 'timezone':'28800', 99 | 'format': '1', 100 | 'timeStamp': str(timestamp), 101 | } 102 | async with self.session.get(url, headers=headers, params=params) as response: 103 | result = await response.json() 104 | result_code = result["response"]["resultCode"] 105 | if result_code == 0: 106 | device_list = result["response"]["deviceList"] 107 | device_wifi_id_dict = dict() 108 | for device in device_list: 109 | device_type_name = device["deviceTypeName"] 110 | if device_keywords in device_type_name: 111 | device_wifi_id_dict[device["deviceId"]] = device["wifiId"] 112 | return device_wifi_id_dict 113 | else: 114 | return None 115 | 116 | 117 | class HiSenseAC: 118 | def __init__(self, wifi_id, device_id, refresh_token, session): 119 | self.wifi_id = wifi_id 120 | self.device_id = device_id 121 | self.refresh_token = refresh_token 122 | self.access_token = None 123 | self.session = session 124 | app_name_encoding = "%E6%B5%B7%E4%BF%A1%E6%99%BA%E6%85%A7%E5%AE%B6" 125 | # app_name = "海信智慧家" 126 | # app_name_encoding = urllib.parse.quote(app_name) 127 | self.headers = { 128 | 'Host': 'api-wg.hismarttv.com', 129 | 'Content-Type': 'application/json', 130 | 'Connection': 'keep-alive', 131 | 'Accept': '*/*', 132 | 'User-Agent': f"{app_name_encoding}/4 CFNetwork/1492.0.1 Darwin/23.3.0", 133 | 'Accept-Language': 'zh-CN,zh-Hans;q=0.9', 134 | 'Accept-Encoding': 'gzip, deflate, br', 135 | } 136 | self.refresh_headers = { 137 | 'Host': 'bas-wg.hismarttv.com', 138 | 'Content-Type': 'application/x-www-form-urlencoded', 139 | 'Connection': 'keep-alive', 140 | 'Accept': '*/*', 141 | 'User-Agent': f"{app_name_encoding}/4 CFNetwork/1492.0.1 Darwin/23.3.0", 142 | 'Accept-Language': 'zh-CN,zh-Hans;q=0.9', 143 | 'Accept-Encoding': 'gzip, deflate, br' 144 | } 145 | self.url_head = "https://api-wg.hismarttv.com/agw/dsg/outer" 146 | self.power_url = f"{self.url_head}/sendDeviceModelCmd?accessToken=" 147 | self.command_url = f"{self.url_head}/uploadRemoteLogicCmd?accessToken=" 148 | self.check_url = f"{self.url_head}/getDeviceLogicalStatusArray?accessToken=" 149 | self.refresh_url = "https://bas-wg.hismarttv.com/aaa/refresh_token2" 150 | self.power_data_template = { 151 | "wifiId": wifi_id, 152 | "deviceId": device_id, 153 | "extendParam": "1", 154 | "cmdVersion": "0", 155 | } 156 | self.check_data_template = { 157 | "deviceList": [ 158 | { 159 | "wifiId": wifi_id, 160 | "deviceId": device_id, 161 | } 162 | ] 163 | } 164 | self.command_data_template = { 165 | "wifiId": wifi_id, 166 | "deviceId": device_id, 167 | "extendParm": "1", 168 | "cmdVersion": "1684085201", 169 | } 170 | self.status = { 171 | "power_on": False, 172 | } 173 | self.hvac_mode_lookup = { 174 | 0: "FAN_ONLY", 175 | 1: "HEAT", 176 | 2: "COOL", 177 | 3: "DRY", 178 | 4: "AUTO", 179 | } 180 | self.fan_mode_lookup = { 181 | 0: "AUTO", 182 | 1: "DIFFUSE", 183 | 2: "LOW", 184 | 3: "MEDIUM", 185 | 4: "HIGH", 186 | } 187 | 188 | async def _send_command(self, url, command_data): 189 | post_url = f"{url}{self.access_token}" 190 | async with self.session.post(post_url, headers=self.headers, json=command_data) as response: 191 | result = await response.json() 192 | result_code = result["response"]["resultCode"] 193 | if result_code == 0: 194 | self.__update(result) 195 | return True 196 | else: 197 | return False 198 | 199 | async def _robust_send_command(self, url, command_data): 200 | if not await self._send_command(url, command_data): 201 | _LOGGER.info("Attempting to refresh token and retry command") 202 | if await self.refresh(): 203 | return await self._send_command(url, command_data) 204 | else: 205 | _LOGGER.error("Failed to refresh token") 206 | return False 207 | 208 | def __update(self, result): 209 | try: 210 | result_list_str = result["response"]["preStatus"] 211 | except KeyError: 212 | try: 213 | result_list_str = result["response"]["deviceStatusList"][0]["deviceStatus"] 214 | 215 | except KeyError: 216 | return False 217 | result_list = [int(i) for i in result_list_str.split(',')] 218 | self.status["desired_temperature"] = result_list[9] 219 | self.status["indoor_temperature"] = result_list[10] 220 | self.status["hvac_mode_id"] = result_list[4] 221 | self.status["hvac_mode"] = self.hvac_mode_lookup[self.status["hvac_mode_id"]] 222 | self.status["fan_mode_id"] = result_list[0] 223 | self.status["fan_mode"] = self.fan_mode_lookup[self.status["fan_mode_id"]] 224 | self.status["screen_on"] = result_list[58] == 1 225 | self.status["power_on"] = result_list[5] == 1 226 | self.status["aux_heat"] = result_list[45] == 1 227 | self.status["nature_wind"] = result_list[44] == 1 228 | self.status["swing_mode_id"] = result_list[209] 229 | 230 | async def turn_on(self): 231 | command_data = deepcopy(self.power_data_template) 232 | command_data["attributes"] = "{\"onAndOff\":\"On\"}" 233 | self.status["power_on"] = True 234 | await self._robust_send_command(self.power_url, command_data) 235 | 236 | async def turn_off(self): 237 | command_data = deepcopy(self.power_data_template) 238 | command_data["attributes"] = "{\"onAndOff\":\"Off\"}" 239 | self.status["power_on"] = False 240 | await self._robust_send_command(self.power_url, command_data) 241 | 242 | async def send_logic_command(self, id: int, param: int): 243 | command_data = deepcopy(self.command_data_template) 244 | command_data["cmdList"] = [ 245 | {"cmdId": id, "cmdOrder": 0, "cmdParm": param, "delayTime": 0} 246 | ] 247 | await self._robust_send_command(self.command_url, command_data) 248 | 249 | async def check_status(self): 250 | await self._robust_send_command(self.check_url, self.check_data_template) 251 | 252 | def get_status(self): 253 | return self.status 254 | 255 | async def refresh(self): 256 | refresh_data = { 257 | 'refreshToken': self.refresh_token, 258 | 'appKey': "1234567890", 259 | 'format': '1', 260 | } 261 | try: 262 | async with self.session.post(self.refresh_url, 263 | headers=self.refresh_headers, 264 | data=refresh_data) as response: 265 | result = await response.json() 266 | self.access_token = result[0]["token"] 267 | _LOGGER.debug(f"Get access token: {self.access_token}") 268 | return True 269 | except: 270 | _LOGGER.error("Failed to refresh token") 271 | return False 272 | -------------------------------------------------------------------------------- /custom_components/hisense/switch.py: -------------------------------------------------------------------------------- 1 | from homeassistant.components.switch import SwitchEntity 2 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 3 | from homeassistant.core import HomeAssistant 4 | from .const import DOMAIN 5 | import logging 6 | 7 | _LOGGER = logging.getLogger(__name__) 8 | 9 | 10 | async def async_setup_entry(hass, config_entry, async_add_entities): 11 | api = hass.data[DOMAIN][config_entry.entry_id] 12 | entities = [AcScreenSwitch(api[device_id]) for device_id in api] 13 | async_add_entities(entities, True) 14 | entities = [AuxHeatSwitch(api[device_id]) for device_id in api] 15 | async_add_entities(entities, True) 16 | 17 | 18 | class AcScreenSwitch(SwitchEntity): 19 | def __init__(self, api): 20 | self._api = api 21 | self._attr_unique_id = f"{api.device_id}_screen" 22 | self._is_on = True 23 | self._attr_icon = "mdi:clock-digital" 24 | 25 | @property 26 | def device_info(self): 27 | return { 28 | "identifiers": {(DOMAIN, self._api.device_id)}, 29 | "name": "Hisense AC", 30 | "manufacturer": "Hisense", 31 | } 32 | 33 | @property 34 | def name(self): 35 | return "Screen Panel" 36 | 37 | @property 38 | def is_on(self): 39 | return self._is_on 40 | 41 | async def async_turn_on(self): 42 | _LOGGER.debug(f"Turning on screen for {self._attr_unique_id}") 43 | await self._api.send_logic_command(41, 1) 44 | self._is_on = True 45 | await self.async_update() 46 | self.async_schedule_update_ha_state(True) 47 | 48 | async def async_turn_off(self): 49 | _LOGGER.debug(f"Turning off screen for {self._attr_unique_id}") 50 | await self._api.send_logic_command(41, 0) 51 | self._is_on = False 52 | await self.async_update() 53 | self.async_schedule_update_ha_state(True) 54 | 55 | async def async_update(self): 56 | status = self._api.get_status() 57 | self._is_on = status.get("screen_on", True) 58 | 59 | 60 | class AuxHeatSwitch(SwitchEntity): 61 | def __init__(self, api): 62 | self._api = api 63 | self._attr_unique_id = f"{api.device_id}_aux_heat" 64 | self._is_on = False 65 | self._attr_icon = "mdi:heating-coil" 66 | 67 | @property 68 | def device_info(self): 69 | return { 70 | "identifiers": {(DOMAIN, self._api.device_id)}, 71 | "name": "Hisense AC", 72 | "manufacturer": "Hisense", 73 | } 74 | 75 | @property 76 | def name(self): 77 | return "Aux Heat" 78 | 79 | @property 80 | def is_on(self): 81 | return self._is_on 82 | 83 | async def async_turn_on(self): 84 | await self._api.send_logic_command(28, 1) 85 | self._is_on = True 86 | await self.async_update() 87 | self.async_schedule_update_ha_state(True) 88 | 89 | async def async_turn_off(self): 90 | await self._api.send_logic_command(28, 0) 91 | self._is_on = False 92 | await self.async_update() 93 | self.async_schedule_update_ha_state(True) 94 | 95 | async def async_update(self): 96 | status = self._api.get_status() 97 | self._is_on = status.get("aux_heat", False) 98 | -------------------------------------------------------------------------------- /custom_components/hisense/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "title": "HiSense Smart Device", 6 | "description": "Login with App username and password", 7 | "data": { 8 | "username": "App login username, could be the phone number", 9 | "password": "App login password, if not available set it in the app first" 10 | } 11 | }, 12 | "home": { 13 | "title": "HiSense Smart Device", 14 | "description": "Select your home ID" 15 | }, 16 | "device": { 17 | "title": "HiSense Smart Device", 18 | "description": "Select your device(s)" 19 | } 20 | }, 21 | "error": { 22 | "cannot_connect": "Unable to connect to the device", 23 | "invalid_auth": "Failed to login", 24 | "unknown": "An unknown error occurred" 25 | }, 26 | "title": "Hisense AC" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /custom_components/hisense/translations/zh-Hans.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "title": "海信智能设备", 6 | "description": "使用海信智慧家的手机号和密码登录", 7 | "data": { 8 | "username": "海信智慧家登录用户手机号", 9 | "password": "如果没有登录密码请先去海信智慧家App里设置一个" 10 | } 11 | }, 12 | "home": { 13 | "title": "海信智能设备", 14 | "description": "选择海信App中的家庭ID(单选)" 15 | }, 16 | "device": { 17 | "title": "海信智能设备", 18 | "description": "选择需要添加的设备(可多选)" 19 | } 20 | }, 21 | "error": { 22 | "cannot_connect": "无法连接到设备", 23 | "invalid_auth": "登录失败", 24 | "unknown": "发生未知错误" 25 | }, 26 | "title": "海信空调" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Hisense Smart Devices", 3 | "render_readme": true, 4 | "hide_default_branch": true 5 | } --------------------------------------------------------------------------------