Samsung AC

hello do you put the code in esp?

I have also managed to get a token from this unit (2787).

Make sure you have Ubuntu 22.04. 3 LTS (no updates) installed
Then open a terminal and run the following commands:

sudo apt-get install node.js
and
sudo apt-get install npm

If you run node --tls-min-v1.0 and you don’t get an error saying that’s an invalid flag, your node version is probably ok.

Install this plugin by running npm install -g homebridge-plugin-samsung-air-conditioner

So I cd`` d into the directory that contains homebridge-samsung-ac-get-token (which i found out using which homebridge-samsung-ac-get-token)

In my case the path was: /usr/local/bin/homebridge-samsung-ac-get-token
Use now: cd /usr/local/bin/

Then use the following command to get the token. Note that AC must be off. In the terminal it will tell you when to turn it on to make the token visible.homebridge-samsung-ac-get-token AC IP ADDRESS --skipCertificate

Then it gives information about port, host, etc. and also indicates the following:

ā€œPower on the device within the next 30 secondsā€

So turn on your AC and you will receive your token

Source and credits to:

https://www.reddit.com/r/Hassio/comments/k0jta9/how_i_set_up_two_old_samsung_ac_units_using/

1 Like

Hi guys.

Anyone playing with the esphome_samsung_ac faced wrong CRCs?
I did not found any hints anywhere :frowning:

I have obviously the older nonNASA AC. I managed to connect to F1/F2 and temporarily powering the M5 with USB, not from RS485.

And when I plug the AC into power socket I start getting such lines.

[18:47:38][W][samsung_ac:175]: NonNASA: invalid crc - got 54 but should be 48: 32c800fd0035343200c8fe003634

I parsed the message according to https://github.com/DannyDeGaspari/Samsung-HVAC-buscontrol and it seems to be relatively correct.
Length is fine, first and last byte are correct as well, etc.

But the checksum is really incorrect.
I computer manually and the the CRC required by the ESP is the one I computed as well. But obviously in the communication it is different.

Any ideas where to look, what to debug?

I’ve made a bit easier solution for everybody who has an older model (2878).

I have mocked up a script (with the use of ChatGPT using the js script from one of your sources) to get the token from an old version (2878) AC.

It can be used from a HA terminal like python3 .\ac_2878_get_token.py 192.168.1.100 , just need to include in the same folder the ac14k_m.pem file.

The pem file is here.

For controlling the AC the following integration can be used (you can find it in HACS) once you have the token.

And this is the script to get the Token, just save it as ac_2878_get_token.py.

import socket
import ssl
import logging
import re
import argparse

class SamsungAirconditioner:
    def __init__(self, ip, logger):
        self.options = {'ip': ip}
        self.logger = logger
        self.token = None

    def get_token(self, callback):
        if not callable(callback):
            raise ValueError("callback is mandatory for get_token")
        # Setup SSL context with TLSv1 and AES256-SHA cipher
        context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)  # Force TLSv1
        context.set_ciphers('ALL:@SECLEVEL=0')
        
        # Load the certificate you trust (cert.pem)
        context.load_cert_chain(certfile='ac14k_m.pem')
        context.check_hostname = False  # Disable hostname verification for self-signed certs
        context.verify_mode = ssl.CERT_NONE  # Optional certificate verification (allow self-signed)

        try:
            with socket.create_connection((self.options['ip'], 2878)) as raw_socket:
                with context.wrap_socket(raw_socket, server_hostname=self.options['ip']) as tls_socket:
                    self.logger.info('connected', {'ipaddr': self.options['ip'], 'port': 2878, 'tls': True})

                    # Receive loop to handle messages line-by-line
                    buffer = ""
                    while True:
                        data = tls_socket.recv(1024).decode('utf-8')
                        if not data:
                            if not self.token:
                                callback(Exception('premature eof'))
                            break

                        buffer += data
                        while '\n' in buffer:
                            line, buffer = buffer.split('\n', 1)
                            self.logger.debug('read: %s', line.strip())

                            # Handle specific responses
                            if line.strip() == 'DRC-1.00':
                                continue
                            if line.strip() == '<?xml version="1.0" encoding="utf-8" ?><Update Type="InvalidateAccount"/>':
                                tls_socket.sendall(b'<Request Type="GetToken" />\r\n')
                                continue
                            if line.strip() == '<?xml version="1.0" encoding="utf-8" ?><Response Type="GetToken" Status="Ready"/>':
                                self.logger.info('waiting for token - turn on the AC in the next 30 seconds')
                                continue
                            if line.strip() == '<?xml version="1.0" encoding="utf-8" ?><Response Status="Fail" Type="Authenticate" ErrorCode="301" />':
                                callback(Exception('Failed authentication'))
                                return

                            # Check for token in response
                            token_match = re.search(r'Token="(.*?)"', line)
                            if token_match:
                                self.token = token_match.group(1)
                                self.logger.info('authenticated')
                                callback(None, self.token)
                                return

                            # Handle status updates
                            if 'Update Type="Status"' in line:
                                state_match = re.search(r'Attr ID="(.*?)" Value="(.*?)"', line)
                                if state_match:
                                    state = {state_match.group(1): state_match.group(2)}
                                    self.logger.info('stateChange', state)
                                    continue

                            # Handle device state
                            if 'Response Type="DeviceState" Status="Okay"' in line:
                                state = {}
                                attributes = line.split("><")
                                for attr in attributes:
                                    attr_match = re.search(r'Attr ID="(.*?)" Type=".*?" Value="(.*?)"', attr)
                                    if attr_match:
                                        state[attr_match.group(1)] = attr_match.group(2)
                                self.logger.info('stateChange', state)

        except (socket.error, ssl.SSLError) as e:
            if not self.token:
                callback(e)


# Set up a basic logger
logger = logging.getLogger('SamsungAirconditioner')
logger.setLevel(logging.DEBUG)
handler = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)

# Define the callback function to handle the response from get_token
def token_callback(error, token=None):
    if error:
        print("Error: {}".format(error))
    else:
        print("Retrieved token: {}".format(token))

# Set up argument parser for command-line input
parser = argparse.ArgumentParser(description="Samsung Air Conditioner Token Retrieval")
parser.add_argument('ip', type=str, help='IP address of the Samsung Air Conditioner')
args = parser.parse_args()

# Create an instance of the SamsungAirconditioner class with the provided IP
aircon = SamsungAirconditioner(ip=args.ip, logger=logger)

# Call the get_token method, passing the token_callback function
aircon.get_token(token_callback)

@wifi75, @MatAlpi, @sebdoan, @peterbuga, @ventudan

I just released a custom component for ESPHome

Hi friends,

I recently migrated my smart home system from OpenHAB to Home Assistant OS.
I managed to get most of my smart devices working as they should with HA, but I’m struggling with my two Samsung Windfree AC’s.

In OpenHAB I controlled these AC’s with Rest API HTTP commands, which allowed me to control most of the AC’s functions (including Quiet mode).

However, in HA I can’t seem to find the way to send the Quiet command.
I am currently using the SmartThings Custom HACS integration to control my AC’s, but this integration is not allowing my to set Quiet mode.

If I understand correctly, the Rest API method should allow me to send the Quiet command with HA, but so far, I couldn’t figure out how to do it.

Can anyone, please, assist me in configuring the Rest API commands? :pray:

Hi folks,

I managed to edit viesta’s SmartThings Custom HACS integration to add support for Quiet and Fast Turbo modes on Samsung ARTIK051_KRAC_18K OCF Devices.

If you own such AC’s you can simply edit the climate.py file (that is located in the /custom_components/smartthings/ folder) with the following code:


"""Support for climate devices through the SmartThings cloud API."""
from __future__ import annotations

import asyncio
from collections.abc import Iterable, Sequence
import logging

from pysmartthings import Attribute, Capability

from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN, ClimateEntity
from homeassistant.components.climate.const import (
    ATTR_HVAC_MODE,
    ATTR_TARGET_TEMP_HIGH,
    ATTR_TARGET_TEMP_LOW,
    HVACAction,
    HVACMode,
    ClimateEntityFeature,
)
from homeassistant.const import ATTR_TEMPERATURE

from . import SmartThingsEntity
from .const import DATA_BROKERS, DOMAIN, UNIT_MAP

ATTR_OPERATION_STATE = "operation_state"
MODE_TO_STATE = {
    "auto": HVACMode.HEAT_COOL,
    "cool": HVACMode.COOL,
    "eco": HVACMode.AUTO,
    "rush hour": HVACMode.AUTO,
    "emergency heat": HVACMode.HEAT,
    "heat": HVACMode.HEAT,
    "off": HVACMode.OFF,
    "wind": HVACMode.FAN_ONLY,
}
STATE_TO_MODE = {
    HVACMode.HEAT_COOL: "auto",
    HVACMode.COOL: "cool",
    HVACMode.HEAT: "heat",
    HVACMode.OFF: "off",
    HVACMode.FAN_ONLY: "wind",
}

OPERATING_STATE_TO_ACTION = {
    "cooling": HVACAction.COOLING,
    "fan only": HVACAction.FAN,
    "heating": HVACAction.HEATING,
    "idle": HVACAction.IDLE,
    "pending cool": HVACAction.COOLING,
    "pending heat": HVACAction.HEATING,
    "vent economizer": HVACAction.FAN,
}

AC_MODE_TO_STATE = {
    "auto": HVACMode.HEAT_COOL,
    "cool": HVACMode.COOL,
    "dry": HVACMode.DRY,
    "coolClean": HVACMode.COOL,
    "dryClean": HVACMode.DRY,
    "heat": HVACMode.HEAT,
    "heatClean": HVACMode.HEAT,
    "fanOnly": HVACMode.FAN_ONLY,
    "wind": HVACMode.FAN_ONLY,
}
STATE_TO_AC_MODE = {
    HVACMode.HEAT_COOL: "auto",
    HVACMode.COOL: "cool",
    HVACMode.DRY: "dry",
    HVACMode.HEAT: "heat",
    HVACMode.FAN_ONLY: "wind",
}


_LOGGER = logging.getLogger(__name__)


async def async_setup_entry(hass, config_entry, async_add_entities):
    """Add climate entities for a config entry."""
    ac_capabilities = [
        Capability.air_conditioner_mode,
        Capability.air_conditioner_fan_mode,
        Capability.switch,
        Capability.temperature_measurement,
        Capability.thermostat_cooling_setpoint,
    ]

    broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id]
    entities = []
    for device in broker.devices.values():
        if not broker.any_assigned(device.device_id, CLIMATE_DOMAIN):
            continue
        if all(capability in device.capabilities for capability in ac_capabilities):
            entities.append(SmartThingsAirConditioner(device))
        else:
            entities.append(SmartThingsThermostat(device))
    async_add_entities(entities, True)


def get_capabilities(capabilities: Sequence[str]) -> Sequence[str] | None:
    """Return all capabilities supported if minimum required are present."""
    supported = [
        Capability.air_conditioner_mode,
        Capability.air_conditioner_fan_mode,
        "fanOscillationMode",
        Capability.switch,
        Capability.temperature_measurement,
        Capability.thermostat,
        Capability.thermostat_cooling_setpoint,
        Capability.thermostat_fan_mode,
        Capability.thermostat_heating_setpoint,
        Capability.thermostat_mode,
        Capability.thermostat_operating_state,
        Capability.execute,
        "custom.airConditionerOptionalMode",
        "custom.thermostatSetpointControl",
    ]
    # Can have this legacy/deprecated capability
    if Capability.thermostat in capabilities:
        return supported
    # Or must have all of these thermostat capabilities
    thermostat_capabilities = [
        Capability.temperature_measurement,
        Capability.thermostat_cooling_setpoint,
        Capability.thermostat_heating_setpoint,
        Capability.thermostat_mode,
    ]
    if all(capability in capabilities for capability in thermostat_capabilities):
        return supported
    # Or must have all of these A/C capabilities
    ac_capabilities = [
        Capability.air_conditioner_mode,
        Capability.air_conditioner_fan_mode,
        Capability.switch,
        Capability.temperature_measurement,
        Capability.thermostat_cooling_setpoint,
    ]
    if all(capability in capabilities for capability in ac_capabilities):
        return supported
    return None


class SmartThingsThermostat(SmartThingsEntity, ClimateEntity):
    """Define a SmartThings climate entities."""

    def __init__(self, device):
        """Init the class."""
        super().__init__(device)
        self._supported_features = self._determine_features()
        self._hvac_mode = None
        self._hvac_modes = None

    def _determine_features(self):
        flags = ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
        if self._device.get_capability(
            Capability.thermostat_fan_mode, Capability.thermostat
        ):
            flags |= ClimateEntityFeature.FAN_MODE
        return flags

    async def async_set_fan_mode(self, fan_mode):
        """Set new target fan mode."""
        await self._device.set_thermostat_fan_mode(fan_mode, set_status=True)

        # State is set optimistically in the command above, therefore update
        # the entity state ahead of receiving the confirming push updates
        self.async_schedule_update_ha_state(True)

    async def async_set_hvac_mode(self, hvac_mode):
        """Set new target operation mode."""
        mode = STATE_TO_MODE[hvac_mode]
        await self._device.set_thermostat_mode(mode, set_status=True)

        # State is set optimistically in the command above, therefore update
        # the entity state ahead of receiving the confirming push updates
        self.async_schedule_update_ha_state(True)

    async def async_set_temperature(self, **kwargs):
        """Set new operation mode and target temperatures."""
        # Operation state
        if operation_state := kwargs.get(ATTR_HVAC_MODE):
            mode = STATE_TO_MODE[operation_state]
            await self._device.set_thermostat_mode(mode, set_status=True)
            await self.async_update()

        # Heat/cool setpoint
        heating_setpoint = None
        cooling_setpoint = None
        if self.hvac_mode == HVACMode.HEAT:
            heating_setpoint = kwargs.get(ATTR_TEMPERATURE)
        elif self.hvac_mode == HVACMode.COOL:
            cooling_setpoint = kwargs.get(ATTR_TEMPERATURE)
        else:
            heating_setpoint = kwargs.get(ATTR_TARGET_TEMP_LOW)
            cooling_setpoint = kwargs.get(ATTR_TARGET_TEMP_HIGH)
        tasks = []
        if heating_setpoint is not None:
            tasks.append(
                self._device.set_heating_setpoint(
                    round(heating_setpoint, 3), set_status=True
                )
            )
        if cooling_setpoint is not None:
            tasks.append(
                self._device.set_cooling_setpoint(
                    round(cooling_setpoint, 3), set_status=True
                )
            )
        await asyncio.gather(*tasks)

        # State is set optimistically in the commands above, therefore update
        # the entity state ahead of receiving the confirming push updates
        self.async_schedule_update_ha_state(True)

    async def async_update(self):
        """Update the attributes of the climate device."""
        thermostat_mode = self._device.status.thermostat_mode
        self._hvac_mode = MODE_TO_STATE.get(thermostat_mode)
        if self._hvac_mode is None:
            _LOGGER.debug(
                "Device %s (%s) returned an invalid hvac mode: %s",
                self._device.label,
                self._device.device_id,
                thermostat_mode,
            )

        modes = set()
        supported_modes = self._device.status.supported_thermostat_modes
        if isinstance(supported_modes, Iterable):
            for mode in supported_modes:
                if (state := MODE_TO_STATE.get(mode)) is not None:
                    modes.add(state)
                else:
                    _LOGGER.debug(
                        "Device %s (%s) returned an invalid supported thermostat mode: %s",
                        self._device.label,
                        self._device.device_id,
                        mode,
                    )
        else:
            _LOGGER.debug(
                "Device %s (%s) returned invalid supported thermostat modes: %s",
                self._device.label,
                self._device.device_id,
                supported_modes,
            )
        self._hvac_modes = list(modes)

    @property
    def current_humidity(self):
        """Return the current humidity."""
        return self._device.status.humidity

    @property
    def current_temperature(self):
        """Return the current temperature."""
        return self._device.status.temperature

    @property
    def fan_mode(self):
        """Return the fan setting."""
        return self._device.status.thermostat_fan_mode

    @property
    def fan_modes(self):
        """Return the list of available fan modes."""
        return self._device.status.supported_thermostat_fan_modes

    @property
    def hvac_action(self) -> str | None:
        """Return the current running hvac operation if supported."""
        return OPERATING_STATE_TO_ACTION.get(
            self._device.status.thermostat_operating_state
        )

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

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

    @property
    def supported_features(self):
        """Return the supported features."""
        return self._supported_features

    @property
    def target_temperature(self):
        """Return the temperature we try to reach."""
        if self.hvac_mode == HVACMode.COOL:
            return self._device.status.cooling_setpoint
        if self.hvac_mode == HVACMode.HEAT:
            return self._device.status.heating_setpoint
        return None

    @property
    def target_temperature_high(self):
        """Return the highbound target temperature we try to reach."""
        if self.hvac_mode == HVACMode.HEAT_COOL:
            return self._device.status.cooling_setpoint
        return None

    @property
    def target_temperature_low(self):
        """Return the lowbound target temperature we try to reach."""
        if self.hvac_mode == HVACMode.HEAT_COOL:
            return self._device.status.heating_setpoint
        return None

    @property
    def temperature_unit(self):
        """Return the unit of measurement."""
        return UNIT_MAP.get(self._device.status.attributes[Attribute.temperature].unit)


class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity):
    """Define a SmartThings Air Conditioner."""

    is_faulty_quiet = False

    def __init__(self, device):
        """Init the class."""
        super().__init__(device)
        self._hvac_modes = None

    async def async_set_fan_mode(self, fan_mode):
        """Set new target fan mode."""
        await self._device.set_fan_mode(fan_mode, set_status=True)
        # State is set optimistically in the command above, therefore update
        # the entity state ahead of receiving the confirming push updates
        self.async_write_ha_state()

    async def async_set_preset_mode(self, preset_mode):
        """Set new target fan mode."""
    #    if self.is_faulty_quiet and preset_mode == "quiet":
    #        result = await self._device.execute(
    #            "mode/convenient/vs/0", {"x.com.samsung.da.modes": "Quiet"}
    #        )
    #    else:
        result = await self._device.command(
            "main",
            "custom.airConditionerOptionalMode",
            "setAcOptionalMode",
            [preset_mode],
        )
        if result:
            self._device.status.update_attribute_value("acOptionalMode", preset_mode)
        self.async_write_ha_state()

    async def async_set_swing_mode(self, swing_mode):
        """Set new target swing mode."""
        # await self._device.set_fan_oscillation_mode(swing_mode, set_status=True)
        result = await self._device.command(
            "main",
            "fanOscillationMode",
            "setFanOscillationMode",
            [swing_mode],
        )
        # State is set optimistically in the command above, therefore update
        # the entity state ahead of receiving the confirming push updates
        if result:
            self._device.status.update_attribute_value("fanOscillationMode", swing_mode)
        self.async_write_ha_state()

    async def async_set_hvac_mode(self, hvac_mode):
        """Set new target operation mode."""
        if hvac_mode == HVACMode.OFF:
            await self.async_turn_off()
            return
        tasks = []
        # Turn on the device if it's off before setting mode.
        if not self._device.status.switch:
            tasks.append(self._device.switch_on(set_status=True))
        tasks.append(
            self._device.set_air_conditioner_mode(
                STATE_TO_AC_MODE[hvac_mode], set_status=True
            )
        )
        await asyncio.gather(*tasks)
        # State is set optimistically in the command above, therefore update
        # the entity state ahead of receiving the confirming push updates
        self.async_write_ha_state()

    async def async_set_temperature(self, **kwargs):
        """Set new target temperature."""
        tasks = []
        # operation mode
        if operation_mode := kwargs.get(ATTR_HVAC_MODE):
            if operation_mode == HVACMode.OFF:
                tasks.append(self._device.switch_off(set_status=True))
            else:
                if not self._device.status.switch:
                    tasks.append(self._device.switch_on(set_status=True))
                tasks.append(self.async_set_hvac_mode(operation_mode))
        # temperature
        tasks.append(
            self._device.set_cooling_setpoint(kwargs[ATTR_TEMPERATURE], set_status=True)
        )
        await asyncio.gather(*tasks)
        # State is set optimistically in the command above, therefore update
        # the entity state ahead of receiving the confirming push updates
        self.async_write_ha_state()

    async def async_turn_on(self):
        """Turn device on."""
        await self._device.switch_on(set_status=True)
        # State is set optimistically in the command above, therefore update
        # the entity state ahead of receiving the confirming push updates
        self.async_write_ha_state()

    async def async_turn_off(self):
        """Turn device off."""
        await self._device.switch_off(set_status=True)
        # State is set optimistically in the command above, therefore update
        # the entity state ahead of receiving the confirming push updates
        self.async_write_ha_state()

    async def async_update(self):
        """Update the calculated fields of the AC."""
        modes = {HVACMode.OFF}
        for mode in self._device.status.supported_ac_modes:
            if (state := AC_MODE_TO_STATE.get(mode)) is not None:
                modes.add(state)
            else:
                _LOGGER.debug(
                    "Device %s (%s) returned an invalid supported AC mode: %s",
                    self._device.label,
                    self._device.device_id,
                    mode,
                )
        self._hvac_modes = list(modes)

    @property
    def current_humidity(self):
        """Return the current humidity."""
        return self._device.status.humidity

    @property
    def current_temperature(self):
        """Return the current temperature."""
        return self._device.status.temperature

    @property
    def extra_state_attributes(self):
        """
        Return device specific state attributes.
        """
        attributes = []
        custom_attributes = []
        state_attributes = {}
        for attribute in attributes:
            value = getattr(self._device.status, attribute)
            if value is not None:
                state_attributes[attribute] = value
        for attribute in custom_attributes:
            value = self._device.status.attributes[attribute].value
            if value is not None:
                state_attributes[attribute] = value
        return state_attributes

    @property
    def fan_mode(self):
        """Return the fan setting."""
        return self._device.status.fan_mode

    @property
    def fan_modes(self):
        """Return the list of available fan modes."""
        return self._device.status.supported_ac_fan_modes

    @property
    def swing_mode(self):
        """Return the swing setting."""
        return self._device.status.attributes["fanOscillationMode"].value

    @property
    def swing_modes(self):
        """Give all swing modes, if attribute is found it most likely works. Samsung gives null, work-around"""
        if (
            self._device.status.attributes["supportedFanOscillationModes"].value
            is not None
        ):
            fan_oscillation_modes = [
                str(x)
                for x in self._device.status.attributes[
                    "supportedFanOscillationModes"
                ].value
            ]
            return fan_oscillation_modes
        elif self._device.status.attributes["fanOscillationMode"].value is not None:
            return ["fixed", "all", "vertical", "horizontal"]
        else:
            return None

    @property
    def preset_mode(self):
        """Return the ac optional mode setting."""

        return self._device.status.attributes["acOptionalMode"].value

    @property
    def preset_modes(self):
        """Return the list of available ac optional modes, in samsung case check that windfree cannot be selected when in heating."""
        restricted_values = ["windFree"]
        model = self._device.status.attributes[Attribute.mnmo].value.split("|")[0]

        supported_ac_optional_modes = [
            str(x)
            for x in self._device.status.attributes["supportedAcOptionalMode"].value
        ]
        if "quiet" not in supported_ac_optional_modes and model == "ARTIK051_KRAC_18K":
            supported_ac_optional_modes.append("quiet")
            self.is_faulty_quiet = True
            
        if "speed" not in supported_ac_optional_modes and model == "ARTIK051_KRAC_18K":
            supported_ac_optional_modes.append("speed")
            self.is_faulty_speed = True

        if self._device.status.air_conditioner_mode in ("auto", "heat"):
            if any(
                restrictedvalue in supported_ac_optional_modes
                for restrictedvalue in restricted_values
            ):
                reduced_supported_optional_modes = supported_ac_optional_modes
                reduced_supported_optional_modes.remove("windFree")
                return reduced_supported_optional_modes
        else:
            return supported_ac_optional_modes

    @property
    def hvac_mode(self):
        """Return current operation ie. heat, cool, idle."""
        if not self._device.status.switch:
            return HVACMode.OFF
        return AC_MODE_TO_STATE.get(self._device.status.air_conditioner_mode)

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

    @property
    def supported_features(self):
        """Return the supported features."""
        supported_ac_optional_modes = [
            str(x)
            for x in self._device.status.attributes["supportedAcOptionalMode"].value
        ]
        if len(supported_ac_optional_modes) == 1 and supported_ac_optional_modes[0] == "off":
            return (
                ClimateEntityFeature.TARGET_TEMPERATURE
                | ClimateEntityFeature.FAN_MODE
                | ClimateEntityFeature.SWING_MODE
            )
        return (
            ClimateEntityFeature.TARGET_TEMPERATURE
            | ClimateEntityFeature.FAN_MODE
            | ClimateEntityFeature.SWING_MODE
            | ClimateEntityFeature.PRESET_MODE
        )

    @property
    def max_temp(self):
        """Return the maximum temperature limit"""
        return int(self._device.status.attributes["maximumSetpoint"].value)

    @property
    def min_temp(self):
        """Return the minimum temperature limit"""
        return int(self._device.status.attributes["minimumSetpoint"].value)

    @property
    def target_temperature(self):
        """Return the temperature we try to reach."""
        return self._device.status.cooling_setpoint

    @property
    def target_temperature_step(self):
        """set the target temperature step size"""
        return 1.0

    @property
    def temperature_unit(self):
        """Return the unit of measurement."""
        return UNIT_MAP.get(self._device.status.attributes[Attribute.temperature].unit)

I hope this helps someone else.

glad that you managed to get it to work.
But why did you use a HACS integration instead of the things that are already provided by HA?
If you say that you could to rest api calls, why not just use
RESTful Command - Home Assistant ?

1 Like

The HACS integration currently offers more functionality compared to the HA SmartThings integration.
For example, with the SmartThings Custom HACS integration you can disable the Beep sounds of the AC’s. The HA SmartThings integration doesn’t support disabling the beep yet.

Regarding why I didn’t use RESTful Command, I’m pretty new to HA and I still don’t know how to configure this method. I tried and failed a few times.

2 Likes

but work also qith samsung AC with 2878 comunication port?

Yes, this custom component works with the old Samsung ACs with the 2878 communication port.

You can use the script from my previous post to get the token.

Hi there, I am trying to get the token using your script.
HAOS is installed in an old laptop.
I am using a desktop PC with win10 (no linux).

I saved the script as .\ac_2878_get_token.py in the custom_components\climate_ip folder of the HAOS system using smb.
when I login into the SSH and try to execute the script I got this error:

python3 .\ac_2878_get_token.py 192.168.0.247

Do I have to mode the script to a different location?

I must have installed python there, to be honest, I don’t know. I thought that HAOS has the python environment installed by default, but don’t worry, the HA container has.

docker exec -it homeassistant /bin/bash

to enter the HA container, and then go to the same directory, and execute the command like you tried, but this is Linux so it will ./ac_2878_get_token.py

Once you executed the script you will need to turn on the AC with the remote, but I think it will give you the instructions. I cannot remember, sorry.

When the AC turns on it will give you the required token and you will be able to set up the integration through the configuration.yaml.

Don’t expect miracles from the AC. The response is quite slow time to time, but generally working.

Let me know if you have any other issues.

Thanks.

The code suggested (docker exec -it homeassistant /bin/bash) gave this output:
bash: docker: command not found

How are you running your HA then? Because that should work without any issues if you have HAOS, and you are accessing it through the SSH addon.

You need a python3 environment to run the script. It is provided inside the HA container, but I have no clue how is that docker command not working for you.

As per this comment, you likely need some further settings to access the container.

Read the comment and the following few others how to access the container. It will get you there.
Use this addon for SSH.

And disable the protection mode.

The page explains how to set it up.

1 Like

Any success? I know it is a life saver this integration as the original app is not working anymore, unless you have a really old phone to run it. Otherwise you are stuck with the remote or an IR Blaster, but then you don’t have any feedback of the values.

1 Like

Dear all, I managed to get the token! thanks for the support.
Now is time to configure climate ip.
edit:
I managed to create the new clima-samsung entity, but looks like it’s not working correctly (yet).
The clima is not sending info about the temperature, or simply siwtching on/off.


climate:
  - platform: climate_ip
    config_file: /config/custom_components/climate_ip/samsung_2878.yaml
    ip_address: 192.168.0.xxx
    token: SDADSAGFDxxxxxxxx
    cert: /config/custom_components/climate_ip/ac14k_m.pem
    mac: 54-xx-xx-xx-xx-xx
    name: 'clima_samsung'
    poll: True

This is how it works for me:

climate:
  - platform: climate_ip
    config_file: '/config/custom_components/climate_ip/samsung_2878.yaml'
    ip_address: '192.168.0.xxx'
    token: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
    cert: 'ac14k_m.pem' 
    mac: 'put the mac address in bc:8c:cd:.... format'
    name: 'AC Living Room'
    poll: True

If it is not working with this setup, then look at the logs. And search for climate_ip. You should see an error message somewhere.

A little progress: seeing temperature settings.
Still not working: unable to send any command.
I opened an issue in github, but at the moment I have no clue on what I can change in terms of configuration files to make it work.

climate:
  - platform: climate_ip
    config_file: '/config/custom_components/climate_ip/samsung_2878.yaml'
    ip_address: '192.168.0.247'
    token: 'x-x-x-x-x'
    cert: '/config/custom_components/climate_ip/ac14k_m.pem'
    mac: 'x-x-x-x-x-x'
    name: 'clima_samsung'
    poll: True
Installation method Home Assistant OS
Core 2025.7.1
Supervisor 2025.07.1
Operating System 16.0
Frontend 20250702.1
Logger: homeassistant.util.loop
Source: util/loop.py:137
First occurred: July 12, 2025 at 7:39:03 PM (2 occurrences)
Last logged: July 12, 2025 at 7:39:03 PM

Detected blocking call to load_verify_locations with args (<ssl.SSLContext object at 0x7f1b84c29400>,) inside the event loop by custom integration 'climate_ip' at custom_components/climate_ip/samsung_2878.py, line 266: sslContext.load_verify_locations(cafile=cfg.cert) (offender: /config/custom_components/climate_ip/samsung_2878.py, line 266: sslContext.load_verify_locations(cafile=cfg.cert)), please create a bug report at https://github.com/atxbyea/samsungrac/issues For developers, please see https://developers.home-assistant.io/docs/asyncio_blocking_operations/#load_verify_locations Traceback (most recent call last): File "", line 198, in _run_module_as_main File "", line 88, in _run_code File "/usr/src/homeassistant/homeassistant/main.py", line 223, in sys.exit(main()) File "/usr/src/homeassistant/homeassistant/main.py", line 209, in main exit_code = runner.run(runtime_conf) File "/usr/src/homeassistant/homeassistant/runner.py", line 154, in run return loop.run_until_complete(setup_and_run_hass(runtime_config)) File "/usr/local/lib/python3.13/asyncio/base_events.py", line 706, in run_until_complete self.run_forever() File "/usr/local/lib/python3.13/asyncio/base_events.py", line 677, in run_forever self._run_once() File "/usr/local/lib/python3.13/asyncio/base_events.py", line 2034, in _run_once handle._run() File "/usr/local/lib/python3.13/asyncio/events.py", line 89, in _run self._context.run(self._callback, *self._args) File "/config/custom_components/climate_ip/climate.py", line 126, in async_setup_platform device_controller = await create_controller( File "/config/custom_components/climate_ip/controller.py", line 71, in create_controller if await c.initialize(): File "/config/custom_components/climate_ip/controller_yaml.py", line 177, in initialize self.update_state() File "/config/custom_components/climate_ip/controller_yaml.py", line 229, in update_state self._state_getter.update_state(self._state_getter.value, debug) File "/config/custom_components/climate_ip/properties.py", line 254, in update_state device_state = self.get_connection(None).execute( File "/config/custom_components/climate_ip/samsung_2878.py", line 323, in execute self.send_socket_command(message, 1) File "/config/custom_components/climate_ip/samsung_2878.py", line 223, in send_socket_command sslSocket = self.socket File "/config/custom_components/climate_ip/samsung_2878.py", line 296, in socket self.create_connection() File "/config/custom_components/climate_ip/samsung_2878.py", line 266, in create_connection sslContext.load_verify_locations(cafile=cfg.cert)
Detected blocking call to load_cert_chain with args (<ssl.SSLContext object at 0x7f1b84c29400>, '/config/custom_components/climate_ip/ac14k_m.pem') inside the event loop by custom integration 'climate_ip' at custom_components/climate_ip/samsung_2878.py, line 268: sslContext.load_cert_chain(cfg.cert) (offender: /config/custom_components/climate_ip/samsung_2878.py, line 268: sslContext.load_cert_chain(cfg.cert)), please create a bug report at https://github.com/atxbyea/samsungrac/issues For developers, please see https://developers.home-assistant.io/docs/asyncio_blocking_operations/#load_cert_chain Traceback (most recent call last): File "", line 198, in _run_module_as_main File "", line 88, in _run_code File "/usr/src/homeassistant/homeassistant/main.py", line 223, in sys.exit(main()) File "/usr/src/homeassistant/homeassistant/main.py", line 209, in main exit_code = runner.run(runtime_conf) File "/usr/src/homeassistant/homeassistant/runner.py", line 154, in run return loop.run_until_complete(setup_and_run_hass(runtime_config)) File "/usr/local/lib/python3.13/asyncio/base_events.py", line 706, in run_until_complete self.run_forever() File "/usr/local/lib/python3.13/asyncio/base_events.py", line 677, in run_forever self._run_once() File "/usr/local/lib/python3.13/asyncio/base_events.py", line 2034, in _run_once handle._run() File "/usr/local/lib/python3.13/asyncio/events.py", line 89, in _run self._context.run(self._callback, *self._args) File "/config/custom_components/climate_ip/climate.py", line 126, in async_setup_platform device_controller = await create_controller( File "/config/custom_components/climate_ip/controller.py", line 71, in create_controller if await c.initialize(): File "/config/custom_components/climate_ip/controller_yaml.py", line 177, in initialize self.update_state() File "/config/custom_components/climate_ip/controller_yaml.py", line 229, in update_state self._state_getter.update_state(self._state_getter.value, debug) File "/config/custom_components/climate_ip/properties.py", line 254, in update_state device_state = self.get_connection(None).execute( File "/config/custom_components/climate_ip/samsung_2878.py", line 323, in execute self.send_socket_command(message, 1) File "/config/custom_components/climate_ip/samsung_2878.py", line 223, in send_socket_command sslSocket = self.socket File "/config/custom_components/climate_ip/samsung_2878.py", line 296, in socket self.create_connection() File "/config/custom_components/climate_ip/samsung_2878.py", line 268, in create_connection sslContext.load_cert_chain(cfg.cert)`