Skip to content

Commit

Permalink
initial light support refactoring for current SmartIR implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
litinoveweedle committed Sep 21, 2024
1 parent a548090 commit 60cc183
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 97 deletions.
27 changes: 27 additions & 0 deletions custom_components/smartir/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,11 @@ async def check_file(file_name, device_data, device_class, check_data):
file_name, device_data, device_class, check_data
):
return True
elif device_class == "light":
if DeviceData.check_file_light(
file_name, device_data, device_class, check_data
):
return True
return False

@staticmethod
Expand Down Expand Up @@ -532,6 +537,11 @@ def check_file_fan(file_name, device_data, device_class, check_data):
def check_file_media_player(file_name, device_data, device_class, check_data):
return True

@staticmethod
def check_file_light(file_name, device_data, device_class, check_data):
return True

# round to given precision
@staticmethod
def precision_round(number, precision):
if precision == 0.1:
Expand All @@ -544,3 +554,20 @@ def precision_round(number, precision):
return round(float(number) / int(precision)) * int(precision)
else:
return None

# find the closest match in a sorted list
@staticmethod
def closest_match(value, list):
prev_val = None
for index, entry in enumerate(list):
if entry > (value or 0):
if prev_val is None:
return index
diff_lo = value - prev_val
diff_hi = entry - value
if diff_lo < diff_hi:
return index - 1
return index
prev_val = entry

return len(list) - 1
193 changes: 97 additions & 96 deletions custom_components/smartir/light.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import asyncio
import json
import logging
import os.path

import voluptuous as vol

Expand All @@ -12,28 +10,28 @@
LightEntity,
PLATFORM_SCHEMA,
)
from homeassistant.const import (
CONF_NAME,
STATE_OFF,
STATE_ON,
)
from homeassistant.core import callback
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant, Event, EventStateChangedData, callback
from homeassistant.helpers.event import async_track_state_change_event, async_call_later
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.restore_state import RestoreEntity
from . import COMPONENT_ABS_DIR, Helper
from .controller import get_controller
from homeassistant.helpers.typing import ConfigType
from . import DeviceData
from .controller import get_controller, get_controller_schema

_LOGGER = logging.getLogger(__name__)

DEFAULT_NAME = "SmartIR Light"
DEFAULT_DELAY = 0.5
DEFAULT_POWER_SENSOR_DELAY = 10

CONF_UNIQUE_ID = "unique_id"
CONF_DEVICE_CODE = "device_code"
CONF_CONTROLLER_DATA = "controller_data"
CONF_DELAY = "delay"
CONF_POWER_SENSOR = "power_sensor"
CONF_POWER_SENSOR_DELAY = "power_sensor_delay"
CONF_POWER_SENSOR_RESTORE_STATE = "power_sensor_restore_state"

CMD_BRIGHTNESS_INCREASE = "brighten"
CMD_BRIGHTNESS_DECREASE = "dim"
Expand All @@ -48,84 +46,39 @@
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(CONF_DEVICE_CODE): cv.positive_int,
vol.Required(CONF_CONTROLLER_DATA): cv.string,
vol.Required(CONF_CONTROLLER_DATA): get_controller_schema(vol, cv),
vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): cv.string,
vol.Optional(CONF_POWER_SENSOR): cv.entity_id,
vol.Optional(
CONF_POWER_SENSOR_DELAY, default=DEFAULT_POWER_SENSOR_DELAY
): cv.positive_int,
vol.Optional(CONF_POWER_SENSOR_RESTORE_STATE, default=True): cv.boolean,
}
)


async def async_setup_platform(
hass,
config,
async_add_entities,
discovery_info=None,
hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None
):
"""Set up the IR Light platform."""
device_code = config.get(CONF_DEVICE_CODE)
device_files_subdir = os.path.join("codes", "light")
device_files_absdir = os.path.join(COMPONENT_ABS_DIR, device_files_subdir)

if not os.path.isdir(device_files_absdir):
os.makedirs(device_files_absdir)

device_json_filename = str(device_code) + ".json"
device_json_path = os.path.join(device_files_absdir, device_json_filename)

if not os.path.exists(device_json_path):
_LOGGER.warning(
"Couldn't find the device Json file. The component "
"will try to download it from the Github repo."
_LOGGER.debug("Setting up the SmartIR light platform")
if not (
device_data := await DeviceData.load_file(
config.get(CONF_DEVICE_CODE),
"light",
{},
hass,
)

try:
codes_source = (
"https://raw.githubusercontent.com/"
"smartHomeHub/SmartIR/master/"
"codes/light/{}.json"
)

await Helper.downloader(
codes_source.format(device_code),
device_json_path,
)
except Exception:
_LOGGER.error(
"There was an error while downloading the device Json file. "
"Please check your internet connection or if the device code "
"exists on GitHub. If the problem still exists please "
"place the file manually in the proper directory."
)
return

with open(device_json_path) as j:
try:
device_data = json.load(j)
except Exception:
_LOGGER.error("The device JSON file is invalid")
return
):
_LOGGER.error("SmartIR light device data init failed!")
return

async_add_entities([SmartIRLight(hass, config, device_data)])


# find the closest match in a sorted list
def closest_match(value, list):
prev_val = None
for index, entry in enumerate(list):
if entry > (value or 0):
if prev_val is None:
return index
diff_lo = value - prev_val
diff_hi = entry - value
if diff_lo < diff_hi:
return index - 1
return index
prev_val = entry

return len(list) - 1


class SmartIRLight(LightEntity, RestoreEntity):
_attr_should_poll = False

def __init__(self, hass, config, device_data):
self.hass = hass
self._unique_id = config.get(CONF_UNIQUE_ID)
Expand All @@ -134,6 +87,16 @@ def __init__(self, hass, config, device_data):
self._controller_data = config.get(CONF_CONTROLLER_DATA)
self._delay = config.get(CONF_DELAY)
self._power_sensor = config.get(CONF_POWER_SENSOR)
self._power_sensor_delay = config.get(CONF_POWER_SENSOR_DELAY)
self._power_sensor_restore_state = config.get(CONF_POWER_SENSOR_RESTORE_STATE)

self._power = STATE_ON
self._brightness = None
self._colortemp = None
self._on_by_remote = False
self._support_color_mode = ColorMode.UNKNOWN
self._power_sensor_check_expect = None
self._power_sensor_check_cancel = None

self._manufacturer = device_data["manufacturer"]
self._supported_models = device_data["supportedModels"]
Expand All @@ -143,14 +106,6 @@ def __init__(self, hass, config, device_data):
self._colortemps = device_data["colorTemperature"]
self._commands = device_data["commands"]

self._power = STATE_ON
self._brightness = None
self._colortemp = None

self._temp_lock = asyncio.Lock()
self._on_by_remote = False
self._support_color_mode = ColorMode.UNKNOWN

if (
CMD_COLORMODE_COLDER in self._commands
and CMD_COLORMODE_WARMER in self._commands
Expand All @@ -176,6 +131,9 @@ def __init__(self, hass, config, device_data):
):
self._support_color_mode = ColorMode.ONOFF

# Init exclusive lock for sending IR commands
self._temp_lock = asyncio.Lock()

# Init the IR/RF controller
self._controller = get_controller(
self.hass,
Expand Down Expand Up @@ -268,8 +226,8 @@ async def async_turn_on(self, **params):
and ColorMode.COLOR_TEMP == self._support_color_mode
):
target = params.get(ATTR_COLOR_TEMP_KELVIN)
old_color_temp = closest_match(self._colortemp, self._colortemps)
new_color_temp = closest_match(target, self._colortemps)
old_color_temp = DeviceData.closest_match(self._colortemp, self._colortemps)
new_color_temp = DeviceData.closest_match(target, self._colortemps)
_LOGGER.debug(
f"Changing color temp from {self._colortemp}K step {old_color_temp} to {target}K step {new_color_temp}"
)
Expand Down Expand Up @@ -302,8 +260,10 @@ async def async_turn_on(self, **params):

elif self._brightnesses:
target = params.get(ATTR_BRIGHTNESS)
old_brightness = closest_match(self._brightness, self._brightnesses)
new_brightness = closest_match(target, self._brightnesses)
old_brightness = DeviceData.closest_match(
self._brightness, self._brightnesses
)
new_brightness = DeviceData.closest_match(target, self._brightnesses)
did_something = True
_LOGGER.debug(
f"Changing brightness from {self._brightness} step {old_brightness} to {target} step {new_brightness}"
Expand Down Expand Up @@ -361,21 +321,62 @@ async def send_command(self, cmd, count=1):
except Exception as e:
_LOGGER.exception(e)

@callback
async def _async_power_sensor_changed(self, event):
async def _async_power_sensor_changed(
self, event: Event[EventStateChangedData]
) -> None:
"""Handle power sensor changes."""
old_state = event.data["old_state"]
new_state = event.data["new_state"]
if new_state is None:
return
old_state = event.data["old_state"]
if new_state.state == old_state.state:

if old_state is not None and new_state.state == old_state.state:
return

if new_state.state == STATE_ON:
if new_state.state == STATE_ON and self._state == STATE_OFF:
self._state = STATE_ON
self._on_by_remote = True
await self.async_write_ha_state()

if new_state.state == STATE_OFF:
elif new_state.state == STATE_OFF:
self._on_by_remote = False
self._power = STATE_OFF
await self.async_write_ha_state()
if self._state == STATE_ON:
self._state = STATE_OFF
self.async_write_ha_state()

@callback
def _async_power_sensor_check_schedule(self, state):
if self._power_sensor_check_cancel:
self._power_sensor_check_cancel()
self._power_sensor_check_cancel = None
self._power_sensor_check_expect = None

@callback
def _async_power_sensor_check(*_):
self._power_sensor_check_cancel = None
expected_state = self._power_sensor_check_expect
self._power_sensor_check_expect = None
current_state = getattr(
self.hass.states.get(self._power_sensor), "state", None
)
_LOGGER.debug(
"Executing power sensor check for expected state '%s', current state '%s'.",
expected_state,
current_state,
)

if (
expected_state in [STATE_ON, STATE_OFF]
and current_state in [STATE_ON, STATE_OFF]
and expected_state != current_state
):
self._state = current_state
_LOGGER.debug(
"Power sensor check failed, reverted device state to '%s'.",
self._state,
)
self.async_write_ha_state()

self._power_sensor_check_expect = state
self._power_sensor_check_cancel = async_call_later(
self.hass, self._power_sensor_delay, _async_power_sensor_check
)
_LOGGER.debug("Scheduled power sensor check for '%s' state.", state)
6 changes: 6 additions & 0 deletions docs/LIGHT_CODES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
## Available codes for Light devices

The following are the code files created by the amazing people in the community. Before you start creating your own code file, try if one of them works for your device. **Please clone this repo and open Pull Request to include your own working and not included codes in the supported models.** Contributing to your own code files is most welcome.

<!-- MARKDOWN-AUTO-DOCS:START (JSON_TO_HTML_TABLE:src=./docs/light_codes.json) -->
<!-- MARKDOWN-AUTO-DOCS:END -->
3 changes: 2 additions & 1 deletion test_device_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
},
"fan": {},
"media_player": {},
"light": {},
}


Expand Down Expand Up @@ -43,7 +44,7 @@ async def test_json(file_path, docs):
async def main():
exit = 0
generate_docs = False
docs = {"climate": [], "fan": [], "media_player": []}
docs = {"climate": [], "fan": [], "media_player": [], "light": []}

files = sys.argv
files.pop(0)
Expand Down

0 comments on commit 60cc183

Please sign in to comment.