17 changed files with 986 additions and 4 deletions
-
5.env-services-dist
-
9.gitignore
-
3README.md
-
22docker-compose.yml
-
1etc/config/automations.yaml
-
50etc/config/blueprints/automation/homeassistant/motion_light.yaml
-
43etc/config/blueprints/automation/homeassistant/notify_leaving_zone.yaml
-
64etc/config/configuration.yaml
-
316etc/config/custom_components/circadian_lighting/__init__.py
-
8etc/config/custom_components/circadian_lighting/manifest.json
-
104etc/config/custom_components/circadian_lighting/sensor.py
-
2etc/config/custom_components/circadian_lighting/services.yaml
-
362etc/config/custom_components/circadian_lighting/switch.py
-
0etc/config/groups.yaml
-
1etc/config/recorder.yaml
-
0etc/config/scenes.yaml
-
0etc/config/scripts.yaml
@ -0,0 +1,5 @@ |
|||
# homeassistant_db |
|||
MYSQL_DATABASE= |
|||
MYSQL_USER= |
|||
MYSQL_ROOT_PASSWORD= |
|||
MYSQL_PASSWORD= |
@ -0,0 +1,9 @@ |
|||
/etc/config/.cloud |
|||
/etc/config/.storage |
|||
/etc/config/deps |
|||
/etc/config/tts |
|||
/etc/config/.HA_VERSION |
|||
/etc/config/home-assistant.log |
|||
/etc/config/secrets.yaml |
|||
/etc/homeassistant_db |
|||
/.env-homeassistant_db |
@ -1,20 +1,34 @@ |
|||
version: "3" |
|||
|
|||
services: |
|||
home-assistant: |
|||
homeassistant_instance: |
|||
image: homeassistant/home-assistant:2021.2.3 |
|||
container_name: home-assistant |
|||
hostname: home-assistant |
|||
container_name: homeassistant_instance |
|||
hostname: homeassistant_instance |
|||
restart: always |
|||
depends_on: |
|||
- homeassistant_db |
|||
|
|||
logging: |
|||
driver: json-file |
|||
options: |
|||
max-size: "10m" |
|||
max-file: "5" |
|||
|
|||
volumes: |
|||
- /etc/localtime:/etc/localtime:ro |
|||
- ./etc/config:/config |
|||
|
|||
ports: |
|||
- "8123:8123" |
|||
homeassistant_db: |
|||
image: mariadb:10.5 |
|||
container_name: homeassistant_db |
|||
|
|||
hostname: homeassistant_db |
|||
restart: unless-stopped |
|||
|
|||
env_file: |
|||
- ./.env-homeassistant_db |
|||
|
|||
volumes: |
|||
- ./etc/homeassistant_db/data:/var/lib/mysql |
@ -0,0 +1 @@ |
|||
[] |
@ -0,0 +1,50 @@ |
|||
blueprint: |
|||
name: Motion-activated Light |
|||
description: Turn on a light when motion is detected. |
|||
domain: automation |
|||
source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/automation/blueprints/motion_light.yaml |
|||
input: |
|||
motion_entity: |
|||
name: Motion Sensor |
|||
selector: |
|||
entity: |
|||
domain: binary_sensor |
|||
device_class: motion |
|||
light_target: |
|||
name: Light |
|||
selector: |
|||
target: |
|||
entity: |
|||
domain: light |
|||
no_motion_wait: |
|||
name: Wait time |
|||
description: Time to leave the light on after last motion is detected. |
|||
default: 120 |
|||
selector: |
|||
number: |
|||
min: 0 |
|||
max: 3600 |
|||
unit_of_measurement: seconds |
|||
|
|||
# If motion is detected within the delay, |
|||
# we restart the script. |
|||
mode: restart |
|||
max_exceeded: silent |
|||
|
|||
trigger: |
|||
platform: state |
|||
entity_id: !input motion_entity |
|||
from: "off" |
|||
to: "on" |
|||
|
|||
action: |
|||
- service: light.turn_on |
|||
target: !input light_target |
|||
- wait_for_trigger: |
|||
platform: state |
|||
entity_id: !input motion_entity |
|||
from: "on" |
|||
to: "off" |
|||
- delay: !input no_motion_wait |
|||
- service: light.turn_off |
|||
target: !input light_target |
@ -0,0 +1,43 @@ |
|||
blueprint: |
|||
name: Zone Notification |
|||
description: Send a notification to a device when a person leaves a specific zone. |
|||
domain: automation |
|||
source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml |
|||
input: |
|||
person_entity: |
|||
name: Person |
|||
selector: |
|||
entity: |
|||
domain: person |
|||
zone_entity: |
|||
name: Zone |
|||
selector: |
|||
entity: |
|||
domain: zone |
|||
notify_device: |
|||
name: Device to notify |
|||
description: Device needs to run the official Home Assistant app to receive notifications. |
|||
selector: |
|||
device: |
|||
integration: mobile_app |
|||
|
|||
trigger: |
|||
platform: state |
|||
entity_id: !input person_entity |
|||
|
|||
variables: |
|||
zone_entity: !input zone_entity |
|||
# This is the state of the person when it's in this zone. |
|||
zone_state: "{{ states[zone_entity].name }}" |
|||
person_entity: !input person_entity |
|||
person_name: "{{ states[person_entity].name }}" |
|||
|
|||
condition: |
|||
condition: template |
|||
value_template: "{{ trigger.from_state.state == zone_state and trigger.to_state.state != zone_state }}" |
|||
|
|||
action: |
|||
domain: mobile_app |
|||
type: notify |
|||
device_id: !input notify_device |
|||
message: "{{ person_name }} has left {{ zone_state }}" |
@ -0,0 +1,64 @@ |
|||
homeassistant: |
|||
name: Home Assistant KR |
|||
latitude: !secret latitude_home |
|||
longitude: !secret longitude_home |
|||
elevation: !secret elevation_home |
|||
temperature_unit: C |
|||
time_zone: Europe/Moscow |
|||
unit_system: metric |
|||
|
|||
zone: |
|||
- name: Home |
|||
latitude: !secret latitude_home |
|||
longitude: !secret longitude_home |
|||
radius: 200 |
|||
icon: mdi:home |
|||
|
|||
default_config: |
|||
|
|||
tts: |
|||
- platform: google_translate |
|||
|
|||
automation: !include automations.yaml |
|||
group: !include groups.yaml |
|||
recorder: !include recorder.yaml |
|||
scene: !include scenes.yaml |
|||
script: !include scripts.yaml |
|||
|
|||
# https://www.home-assistant.io/integrations/yeelight |
|||
yeelight: |
|||
devices: |
|||
10.10.10.10: |
|||
name: Room unit0 |
|||
transition: 1000 |
|||
use_music_mode: false |
|||
save_on_change: false |
|||
model: color4 |
|||
10.10.10.11: |
|||
name: Room unit1 |
|||
transition: 1000 |
|||
use_music_mode: false |
|||
save_on_change: false |
|||
model: color4 |
|||
10.10.10.12: |
|||
name: Room unit2 |
|||
transition: 1000 |
|||
use_music_mode: false |
|||
save_on_change: false |
|||
model: color4 |
|||
10.10.10.13: |
|||
name: Room unit3 |
|||
transition: 1000 |
|||
use_music_mode: false |
|||
save_on_change: false |
|||
model: color4 |
|||
|
|||
# https://github.com/claytonjn/hass-circadian_lighting |
|||
circadian_lighting: |
|||
switch: |
|||
- platform: circadian_lighting |
|||
lights_ct: |
|||
- light.room_unit0 |
|||
- light.room_unit1 |
|||
- light.room_unit2 |
|||
- light.room_unit3 |
@ -0,0 +1,316 @@ |
|||
""" |
|||
Circadian Lighting Component for Home-Assistant. |
|||
|
|||
This component calculates color temperature and brightness to synchronize |
|||
your color changing lights with perceived color temperature of the sky throughout |
|||
the day. This gives your environment a more natural feel, with cooler whites during |
|||
the midday and warmer tints near twilight and dawn. |
|||
|
|||
In addition, the component sets your lights to a nice warm white at 1% in "Sleep" mode, |
|||
which is far brighter than starlight but won't reset your circadian rhythm or break down |
|||
too much rhodopsin in your eyes. |
|||
|
|||
Human circadian rhythms are heavily influenced by ambient light levels and |
|||
hues. Hormone production, brainwave activity, mood and wakefulness are |
|||
just some of the cognitive functions tied to cyclical natural light. |
|||
http://en.wikipedia.org/wiki/Zeitgeber |
|||
|
|||
Here's some further reading: |
|||
|
|||
http://www.cambridgeincolour.com/tutorials/sunrise-sunset-calculator.htm |
|||
http://en.wikipedia.org/wiki/Color_temperature |
|||
|
|||
Technical notes: I had to make a lot of assumptions when writing this app |
|||
* There are no considerations for weather or altitude, but does use your |
|||
hub's location to calculate the sun position. |
|||
* The component doesn't calculate a true "Blue Hour" -- it just sets the |
|||
lights to 2700K (warm white) until your hub goes into Night mode |
|||
""" |
|||
|
|||
import logging |
|||
|
|||
import voluptuous as vol |
|||
|
|||
import homeassistant.helpers.config_validation as cv |
|||
from homeassistant.components.light import ( |
|||
VALID_TRANSITION, ATTR_TRANSITION) |
|||
from homeassistant.const import ( |
|||
CONF_LATITUDE, CONF_LONGITUDE, CONF_ELEVATION, |
|||
SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET) |
|||
from homeassistant.util import Throttle |
|||
from homeassistant.helpers.discovery import load_platform |
|||
from homeassistant.helpers.dispatcher import dispatcher_send |
|||
from homeassistant.helpers.event import track_sunrise, track_sunset, track_time_change |
|||
from homeassistant.util.color import ( |
|||
color_temperature_to_rgb, color_RGB_to_xy, |
|||
color_xy_to_hs) |
|||
from homeassistant.util.dt import now as dt_now, get_time_zone |
|||
|
|||
from datetime import datetime, timedelta |
|||
|
|||
VERSION = '1.0.13' |
|||
|
|||
_LOGGER = logging.getLogger(__name__) |
|||
|
|||
DOMAIN = 'circadian_lighting' |
|||
CIRCADIAN_LIGHTING_PLATFORMS = ['sensor', 'switch'] |
|||
CIRCADIAN_LIGHTING_UPDATE_TOPIC = '{0}_update'.format(DOMAIN) |
|||
DATA_CIRCADIAN_LIGHTING = 'data_cl' |
|||
|
|||
CONF_MIN_CT = 'min_colortemp' |
|||
DEFAULT_MIN_CT = 2500 |
|||
CONF_MAX_CT = 'max_colortemp' |
|||
DEFAULT_MAX_CT = 5500 |
|||
CONF_SUNRISE_OFFSET = 'sunrise_offset' |
|||
CONF_SUNSET_OFFSET = 'sunset_offset' |
|||
CONF_SUNRISE_TIME = 'sunrise_time' |
|||
CONF_SUNSET_TIME = 'sunset_time' |
|||
CONF_INTERVAL = 'interval' |
|||
DEFAULT_INTERVAL = 300 |
|||
DEFAULT_TRANSITION = 60 |
|||
|
|||
CONFIG_SCHEMA = vol.Schema({ |
|||
DOMAIN: vol.Schema({ |
|||
vol.Optional(CONF_MIN_CT, default=DEFAULT_MIN_CT): |
|||
vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), |
|||
vol.Optional(CONF_MAX_CT, default=DEFAULT_MAX_CT): |
|||
vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), |
|||
vol.Optional(CONF_SUNRISE_OFFSET): cv.time_period_str, |
|||
vol.Optional(CONF_SUNSET_OFFSET): cv.time_period_str, |
|||
vol.Optional(CONF_SUNRISE_TIME): cv.time, |
|||
vol.Optional(CONF_SUNSET_TIME): cv.time, |
|||
vol.Optional(CONF_LATITUDE): cv.latitude, |
|||
vol.Optional(CONF_LONGITUDE): cv.longitude, |
|||
vol.Optional(CONF_ELEVATION): float, |
|||
vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.positive_int, |
|||
vol.Optional(ATTR_TRANSITION, default=DEFAULT_TRANSITION): VALID_TRANSITION |
|||
}), |
|||
}, extra=vol.ALLOW_EXTRA) |
|||
|
|||
def setup(hass, config): |
|||
"""Set up the Circadian Lighting component.""" |
|||
conf = config[DOMAIN] |
|||
min_colortemp = conf.get(CONF_MIN_CT) |
|||
max_colortemp = conf.get(CONF_MAX_CT) |
|||
sunrise_offset = conf.get(CONF_SUNRISE_OFFSET) |
|||
sunset_offset = conf.get(CONF_SUNSET_OFFSET) |
|||
sunrise_time = conf.get(CONF_SUNRISE_TIME) |
|||
sunset_time = conf.get(CONF_SUNSET_TIME) |
|||
|
|||
latitude = conf.get(CONF_LATITUDE, hass.config.latitude) |
|||
longitude = conf.get(CONF_LONGITUDE, hass.config.longitude) |
|||
elevation = conf.get(CONF_ELEVATION, hass.config.elevation) |
|||
|
|||
load_platform(hass, 'sensor', DOMAIN, {}, config) |
|||
|
|||
interval = conf.get(CONF_INTERVAL) |
|||
transition = conf.get(ATTR_TRANSITION) |
|||
|
|||
cl = CircadianLighting(hass, min_colortemp, max_colortemp, |
|||
sunrise_offset, sunset_offset, sunrise_time, sunset_time, |
|||
latitude, longitude, elevation, |
|||
interval, transition) |
|||
|
|||
hass.data[DATA_CIRCADIAN_LIGHTING] = cl |
|||
|
|||
return True |
|||
|
|||
class CircadianLighting(object): |
|||
"""Calculate universal Circadian values.""" |
|||
|
|||
def __init__(self, hass, min_colortemp, max_colortemp, |
|||
sunrise_offset, sunset_offset, sunrise_time, sunset_time, |
|||
latitude, longitude, elevation, |
|||
interval, transition): |
|||
self.hass = hass |
|||
self.data = {} |
|||
self.data['min_colortemp'] = min_colortemp |
|||
self.data['max_colortemp'] = max_colortemp |
|||
self.data['sunrise_offset'] = sunrise_offset |
|||
self.data['sunset_offset'] = sunset_offset |
|||
self.data['sunrise_time'] = sunrise_time |
|||
self.data['sunset_time'] = sunset_time |
|||
self.data['latitude'] = latitude |
|||
self.data['longitude'] = longitude |
|||
self.data['elevation'] = elevation |
|||
self.data['interval'] = interval |
|||
self.data['transition'] = transition |
|||
self.data['timezone'] = self.get_timezone() |
|||
self.data['percent'] = self.calc_percent() |
|||
self.data['colortemp'] = self.calc_colortemp() |
|||
self.data['rgb_color'] = self.calc_rgb() |
|||
self.data['xy_color'] = self.calc_xy() |
|||
self.data['hs_color'] = self.calc_hs() |
|||
|
|||
self.update = Throttle(timedelta(seconds=interval))(self._update) |
|||
|
|||
if self.data['sunrise_time'] is not None: |
|||
track_time_change(self.hass, self._update, hour=int(self.data['sunrise_time'].strftime("%H")), minute=int(self.data['sunrise_time'].strftime("%M")), second=int(self.data['sunrise_time'].strftime("%S"))) |
|||
else: |
|||
track_sunrise(self.hass, self._update, self.data['sunrise_offset']) |
|||
if self.data['sunset_time'] is not None: |
|||
track_time_change(self.hass, self._update, hour=int(self.data['sunset_time'].strftime("%H")), minute=int(self.data['sunset_time'].strftime("%M")), second=int(self.data['sunset_time'].strftime("%S"))) |
|||
else: |
|||
track_sunset(self.hass, self._update, self.data['sunset_offset']) |
|||
|
|||
def get_timezone(self): |
|||
from timezonefinder import TimezoneFinder |
|||
tf = TimezoneFinder() |
|||
timezone_string = tf.timezone_at(lng=self.data['longitude'], lat=self.data['latitude']) |
|||
timezone = get_time_zone(timezone_string) |
|||
_LOGGER.debug("Timezone: " + str(timezone)) |
|||
return timezone |
|||
|
|||
def get_sunrise_sunset(self, date = None): |
|||
if self.data['sunrise_time'] is not None and self.data['sunset_time'] is not None: |
|||
if date is None: |
|||
date = dt_now(self.data['timezone']) |
|||
sunrise = date.replace(hour=int(self.data['sunrise_time'].strftime("%H")), minute=int(self.data['sunrise_time'].strftime("%M")), second=int(self.data['sunrise_time'].strftime("%S")), microsecond=int(self.data['sunrise_time'].strftime("%f"))) |
|||
sunset = date.replace(hour=int(self.data['sunset_time'].strftime("%H")), minute=int(self.data['sunset_time'].strftime("%M")), second=int(self.data['sunset_time'].strftime("%S")), microsecond=int(self.data['sunset_time'].strftime("%f"))) |
|||
solar_noon = sunrise + (sunset - sunrise)/2 |
|||
solar_midnight = sunset + ((sunrise + timedelta(days=1)) - sunset)/2 |
|||
else: |
|||
import astral |
|||
location = astral.Location() |
|||
location.name = 'name' |
|||
location.region = 'region' |
|||
location.latitude = self.data['latitude'] |
|||
location.longitude = self.data['longitude'] |
|||
location.elevation = self.data['elevation'] |
|||
_LOGGER.debug("Astral location: " + str(location)) |
|||
if self.data['sunrise_time'] is not None: |
|||
if date is None: |
|||
date = dt_now(self.data['timezone']) |
|||
sunrise = date.replace(hour=int(self.data['sunrise_time'].strftime("%H")), minute=int(self.data['sunrise_time'].strftime("%M")), second=int(self.data['sunrise_time'].strftime("%S")), microsecond=int(self.data['sunrise_time'].strftime("%f"))) |
|||
else: |
|||
sunrise = location.sunrise(date) |
|||
if self.data['sunset_time'] is not None: |
|||
if date is None: |
|||
date = dt_now(self.data['timezone']) |
|||
sunset = date.replace(hour=int(self.data['sunset_time'].strftime("%H")), minute=int(self.data['sunset_time'].strftime("%M")), second=int(self.data['sunset_time'].strftime("%S")), microsecond=int(self.data['sunset_time'].strftime("%f"))) |
|||
else: |
|||
sunset = location.sunset(date) |
|||
solar_noon = location.solar_noon(date) |
|||
solar_midnight = location.solar_midnight(date) |
|||
if self.data['sunrise_offset'] is not None: |
|||
sunrise = sunrise + self.data['sunrise_offset'] |
|||
if self.data['sunset_offset'] is not None: |
|||
sunset = sunset + self.data['sunset_offset'] |
|||
return { |
|||
SUN_EVENT_SUNRISE: sunrise.astimezone(self.data['timezone']), |
|||
SUN_EVENT_SUNSET: sunset.astimezone(self.data['timezone']), |
|||
'solar_noon': solar_noon.astimezone(self.data['timezone']), |
|||
'solar_midnight': solar_midnight.astimezone(self.data['timezone']) |
|||
} |
|||
|
|||
def calc_percent(self): |
|||
now = dt_now(self.data['timezone']) |
|||
_LOGGER.debug("now: " + str(now)) |
|||
|
|||
today_sun_times = self.get_sunrise_sunset(now) |
|||
_LOGGER.debug("today_sun_times: " + str(today_sun_times)) |
|||
|
|||
# Convert everything to epoch timestamps for easy calculation |
|||
now_seconds = now.timestamp() |
|||
sunrise_seconds = today_sun_times[SUN_EVENT_SUNRISE].timestamp() |
|||
sunset_seconds = today_sun_times[SUN_EVENT_SUNSET].timestamp() |
|||
solar_noon_seconds = today_sun_times['solar_noon'].timestamp() |
|||
solar_midnight_seconds = today_sun_times['solar_midnight'].timestamp() |
|||
|
|||
if now < today_sun_times[SUN_EVENT_SUNRISE]: # It's before sunrise (after midnight) |
|||
# Because it's before sunrise (and after midnight) sunset must have happend yesterday |
|||
yesterday_sun_times = self.get_sunrise_sunset(now - timedelta(days=1)) |
|||
_LOGGER.debug("yesterday_sun_times: " + str(yesterday_sun_times)) |
|||
sunset_seconds = yesterday_sun_times[SUN_EVENT_SUNSET].timestamp() |
|||
if today_sun_times['solar_midnight'] > today_sun_times[SUN_EVENT_SUNSET] and yesterday_sun_times['solar_midnight'] > yesterday_sun_times[SUN_EVENT_SUNSET]: |
|||
# Solar midnight is after sunset so use yesterdays's time |
|||
solar_midnight_seconds = yesterday_sun_times['solar_midnight'].timestamp() |
|||
elif now > today_sun_times[SUN_EVENT_SUNSET]: # It's after sunset (before midnight) |
|||
# Because it's after sunset (and before midnight) sunrise should happen tomorrow |
|||
tomorrow_sun_times = self.get_sunrise_sunset(now + timedelta(days=1)) |
|||
_LOGGER.debug("tomorrow_sun_times: " + str(tomorrow_sun_times)) |
|||
sunrise_seconds = tomorrow_sun_times[SUN_EVENT_SUNRISE].timestamp() |
|||
if today_sun_times['solar_midnight'] < today_sun_times[SUN_EVENT_SUNRISE] and tomorrow_sun_times['solar_midnight'] < tomorrow_sun_times[SUN_EVENT_SUNRISE]: |
|||
# Solar midnight is before sunrise so use tomorrow's time |
|||
solar_midnight_seconds = tomorrow_sun_times['solar_midnight'].timestamp() |
|||
|
|||
_LOGGER.debug("now_seconds: " + str(now_seconds)) |
|||
_LOGGER.debug("sunrise_seconds: " + str(sunrise_seconds)) |
|||
_LOGGER.debug("sunset_seconds: " + str(sunset_seconds)) |
|||
_LOGGER.debug("solar_midnight_seconds: " + str(solar_midnight_seconds)) |
|||
_LOGGER.debug("solar_noon_seconds: " + str(solar_noon_seconds)) |
|||
|
|||
# Figure out where we are in time so we know which half of the parabola to calculate |
|||
# We're generating a different sunset-sunrise parabola for before and after solar midnight |
|||
# because it might not be half way between sunrise and sunset |
|||
# We're also (obviously) generating a different parabola for sunrise-sunset |
|||
|
|||
# sunrise-sunset parabola |
|||
if now_seconds > sunrise_seconds and now_seconds < sunset_seconds: |
|||
h = solar_noon_seconds |
|||
k = 100 |
|||
# parabola before solar_noon |
|||
if now_seconds < solar_noon_seconds: |
|||
x = sunrise_seconds |
|||
# parabola after solar_noon |
|||
else: |
|||
x = sunset_seconds |
|||
y = 0 |
|||
|
|||
# sunset_sunrise parabola |
|||
elif now_seconds > sunset_seconds and now_seconds < sunrise_seconds: |
|||
h = solar_midnight_seconds |
|||
k = -100 |
|||
# parabola before solar_midnight |
|||
if now_seconds < solar_midnight_seconds: |
|||
x = sunset_seconds |
|||
# parabola after solar_midnight |
|||
else: |
|||
x = sunrise_seconds |
|||
y = 0 |
|||
|
|||
a = (y-k)/(h-x)**2 |
|||
percentage = a*(now_seconds-h)**2+k |
|||
|
|||
_LOGGER.debug("h: " + str(h)) |
|||
_LOGGER.debug("k: " + str(k)) |
|||
_LOGGER.debug("x: " + str(x)) |
|||
_LOGGER.debug("y: " + str(y)) |
|||
_LOGGER.debug("a: " + str(a)) |
|||
_LOGGER.debug("percentage: " + str(percentage)) |
|||
|
|||
return percentage |
|||
|
|||
def calc_colortemp(self): |
|||
if self.data['percent'] > 0: |
|||
return ((self.data['max_colortemp'] - self.data['min_colortemp']) * (self.data['percent'] / 100)) + self.data['min_colortemp'] |
|||
else: |
|||
return self.data['min_colortemp'] |
|||
|
|||
def calc_rgb(self): |
|||
return color_temperature_to_rgb(self.data['colortemp']) |
|||
|
|||
def calc_xy(self): |
|||
rgb = self.calc_rgb() |
|||
iR = rgb[0] |
|||
iG = rgb[1] |
|||
iB = rgb[2] |
|||
|
|||
return color_RGB_to_xy(iR, iG, iB) |
|||
|
|||
def calc_hs(self): |
|||
xy = self.calc_xy() |
|||
vX = xy[0] |
|||
vY = xy[1] |
|||
|
|||
return color_xy_to_hs(vX, vY) |
|||
|
|||
def _update(self, *args, **kwargs): |
|||
"""Update Circadian Values.""" |
|||
self.data['percent'] = self.calc_percent() |
|||
self.data['colortemp'] = self.calc_colortemp() |
|||
self.data['rgb_color'] = self.calc_rgb() |
|||
self.data['xy_color'] = self.calc_xy() |
|||
self.data['hs_color'] = self.calc_hs() |
|||
dispatcher_send(self.hass, CIRCADIAN_LIGHTING_UPDATE_TOPIC) |
|||
_LOGGER.debug("Circadian Lighting Component Updated") |
@ -0,0 +1,8 @@ |
|||
{ |
|||
"domain": "circadian_lighting", |
|||
"name": "Circadian Lighting", |
|||
"documentation": "https://github.com/claytonjn/hass-circadian_lighting", |
|||
"dependencies": [], |
|||
"codeowners": ["@claytonjn"], |
|||
"requirements": ["timezonefinder==4.2.0"] |
|||
} |
@ -0,0 +1,104 @@ |
|||
""" |
|||
Circadian Lighting Sensor for Home-Assistant. |
|||
""" |
|||
|
|||
DEPENDENCIES = ['circadian_lighting'] |
|||
|
|||
import logging |
|||
|
|||
from custom_components.circadian_lighting import DOMAIN, CIRCADIAN_LIGHTING_UPDATE_TOPIC, DATA_CIRCADIAN_LIGHTING |
|||
|
|||
from homeassistant.helpers.dispatcher import dispatcher_connect |
|||
from homeassistant.helpers.entity import Entity |
|||
|
|||
import datetime |
|||
|
|||
_LOGGER = logging.getLogger(__name__) |
|||
|
|||
ICON = 'mdi:theme-light-dark' |
|||
|
|||
def setup_platform(hass, config, add_devices, discovery_info=None): |
|||
"""Set up the Circadian Lighting sensor.""" |
|||
cl = hass.data.get(DATA_CIRCADIAN_LIGHTING) |
|||
if cl: |
|||
cs = CircadianSensor(hass, cl) |
|||
add_devices([cs]) |
|||
|
|||
def update(call=None): |
|||
"""Update component.""" |
|||
cl._update() |
|||
service_name = "values_update" |
|||
hass.services.register(DOMAIN, service_name, update) |
|||
return True |
|||
else: |
|||
return False |
|||
|
|||
class CircadianSensor(Entity): |
|||
"""Representation of a Circadian Lighting sensor.""" |
|||
|
|||
def __init__(self, hass, cl): |
|||
"""Initialize the Circadian Lighting sensor.""" |
|||
self._cl = cl |
|||
self._name = 'Circadian Values' |
|||
self._entity_id = 'sensor.circadian_values' |
|||
self._state = self._cl.data['percent'] |
|||
self._unit_of_measurement = '%' |
|||
self._icon = ICON |
|||
self._hs_color = self._cl.data['hs_color'] |
|||
self._attributes = {} |
|||
self._attributes['colortemp'] = self._cl.data['colortemp'] |
|||
self._attributes['rgb_color'] = self._cl.data['rgb_color'] |
|||
self._attributes['xy_color'] = self._cl.data['xy_color'] |
|||
|
|||
"""Register callbacks.""" |
|||
dispatcher_connect(hass, CIRCADIAN_LIGHTING_UPDATE_TOPIC, self.update_sensor) |
|||
|
|||
@property |
|||
def entity_id(self): |
|||
"""Return the entity ID of the sensor.""" |
|||
return self._entity_id |
|||
|
|||
@property |
|||
def name(self): |
|||
"""Return the name of the sensor.""" |
|||
return self._name |
|||
|
|||
@property |
|||
def state(self): |
|||
"""Return the state of the sensor.""" |
|||
return self._state |
|||
|
|||
@property |
|||
def unit_of_measurement(self): |
|||
"""Return the unit of measurement.""" |
|||
return self._unit_of_measurement |
|||
|
|||
@property |
|||
def icon(self): |
|||
"""Icon to use in the frontend, if any.""" |
|||
return self._icon |
|||
|
|||
@property |
|||
def hs_color(self): |
|||
return self._hs_color |
|||
|
|||
@property |
|||
def device_state_attributes(self): |
|||
"""Return the attributes of the sensor.""" |
|||
return self._attributes |
|||
|
|||
def update(self): |
|||
"""Fetch new state data for the sensor. |
|||
|
|||
This is the only method that should fetch new data for Home Assistant. |
|||
""" |
|||
self._cl.update() |
|||
|
|||
def update_sensor(self): |
|||
if self._cl.data is not None: |
|||
self._state = self._cl.data['percent'] |
|||
self._hs_color = self._cl.data['hs_color'] |
|||
self._attributes['colortemp'] = self._cl.data['colortemp'] |
|||
self._attributes['rgb_color'] = self._cl.data['rgb_color'] |
|||
self._attributes['xy_color'] = self._cl.data['xy_color'] |
|||
_LOGGER.debug("Circadian Lighting Sensor Updated") |
@ -0,0 +1,2 @@ |
|||
values_update: |
|||
description: Updates values for Circadian Lighting. |
@ -0,0 +1,362 @@ |
|||
""" |
|||
Circadian Lighting Switch for Home-Assistant. |
|||
""" |
|||
|
|||
DEPENDENCIES = ['circadian_lighting', 'light'] |
|||
|
|||
import logging |
|||
|
|||
from custom_components.circadian_lighting import DOMAIN, CIRCADIAN_LIGHTING_UPDATE_TOPIC, DATA_CIRCADIAN_LIGHTING |
|||
|
|||
import voluptuous as vol |
|||
|
|||
import homeassistant.helpers.config_validation as cv |
|||
from homeassistant.helpers.dispatcher import dispatcher_connect |
|||
from homeassistant.helpers.event import track_state_change |
|||
from homeassistant.helpers.restore_state import RestoreEntity |
|||
from homeassistant.components.light import ( |
|||
is_on, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION, |
|||
VALID_TRANSITION, ATTR_WHITE_VALUE, ATTR_XY_COLOR, DOMAIN as LIGHT_DOMAIN) |
|||
|
|||
try: |
|||
from homeassistant.components.switch import SwitchEntity |
|||
except ImportError: |
|||
from homeassistant.components.switch import SwitchDevice as SwitchEntity |
|||
|
|||
from homeassistant.const import ( |
|||
ATTR_ENTITY_ID, CONF_NAME, CONF_PLATFORM, STATE_ON, |
|||
SERVICE_TURN_ON) |
|||
from homeassistant.util import slugify |
|||
from homeassistant.util.color import ( |
|||
color_RGB_to_xy, color_temperature_kelvin_to_mired, |
|||
color_temperature_to_rgb, color_xy_to_hs) |
|||
|
|||
_LOGGER = logging.getLogger(__name__) |
|||
|
|||
ICON = 'mdi:theme-light-dark' |
|||
|
|||
CONF_LIGHTS_CT = 'lights_ct' |
|||
CONF_LIGHTS_RGB = 'lights_rgb' |
|||
CONF_LIGHTS_XY = 'lights_xy' |
|||
CONF_LIGHTS_BRIGHT = 'lights_brightness' |
|||
CONF_DISABLE_BRIGHTNESS_ADJUST = 'disable_brightness_adjust' |
|||
CONF_MIN_BRIGHT = 'min_brightness' |
|||
DEFAULT_MIN_BRIGHT = 1 |
|||
CONF_MAX_BRIGHT = 'max_brightness' |
|||
DEFAULT_MAX_BRIGHT = 100 |
|||
CONF_SLEEP_ENTITY = 'sleep_entity' |
|||
CONF_SLEEP_STATE = 'sleep_state' |
|||
CONF_SLEEP_CT = 'sleep_colortemp' |
|||
CONF_SLEEP_BRIGHT = 'sleep_brightness' |
|||
CONF_DISABLE_ENTITY = 'disable_entity' |
|||
CONF_DISABLE_STATE = 'disable_state' |
|||
CONF_INITIAL_TRANSITION = 'initial_transition' |
|||
DEFAULT_INITIAL_TRANSITION = 1 |
|||
|
|||
PLATFORM_SCHEMA = vol.Schema({ |
|||
vol.Required(CONF_PLATFORM): 'circadian_lighting', |
|||
vol.Optional(CONF_NAME, default="Circadian Lighting"): cv.string, |
|||
vol.Optional(CONF_LIGHTS_CT): cv.entity_ids, |
|||
vol.Optional(CONF_LIGHTS_RGB): cv.entity_ids, |
|||
vol.Optional(CONF_LIGHTS_XY): cv.entity_ids, |
|||
vol.Optional(CONF_LIGHTS_BRIGHT): cv.entity_ids, |
|||
vol.Optional(CONF_DISABLE_BRIGHTNESS_ADJUST, default=False): cv.boolean, |
|||
vol.Optional(CONF_MIN_BRIGHT, default=DEFAULT_MIN_BRIGHT): |
|||
vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), |
|||
vol.Optional(CONF_MAX_BRIGHT, default=DEFAULT_MAX_BRIGHT): |
|||
vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), |
|||
vol.Optional(CONF_SLEEP_ENTITY): cv.entity_id, |
|||
vol.Optional(CONF_SLEEP_STATE): cv.string, |
|||
vol.Optional(CONF_SLEEP_CT): |
|||
vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), |
|||
vol.Optional(CONF_SLEEP_BRIGHT): |
|||
vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), |
|||
vol.Optional(CONF_DISABLE_ENTITY): cv.entity_id, |
|||
vol.Optional(CONF_DISABLE_STATE): cv.string, |
|||
vol.Optional(CONF_INITIAL_TRANSITION, default=DEFAULT_INITIAL_TRANSITION): |
|||
VALID_TRANSITION |
|||
}) |
|||
|
|||
def setup_platform(hass, config, add_devices, discovery_info=None): |
|||
"""Set up the Circadian Lighting switches.""" |
|||
cl = hass.data.get(DATA_CIRCADIAN_LIGHTING) |
|||
if cl: |
|||
lights_ct = config.get(CONF_LIGHTS_CT) |
|||
lights_rgb = config.get(CONF_LIGHTS_RGB) |
|||
lights_xy = config.get(CONF_LIGHTS_XY) |
|||
lights_brightness = config.get(CONF_LIGHTS_BRIGHT) |
|||
disable_brightness_adjust = config.get(CONF_DISABLE_BRIGHTNESS_ADJUST) |
|||
name = config.get(CONF_NAME) |
|||
min_brightness = config.get(CONF_MIN_BRIGHT) |
|||
max_brightness = config.get(CONF_MAX_BRIGHT) |
|||
sleep_entity = config.get(CONF_SLEEP_ENTITY) |
|||
sleep_state = config.get(CONF_SLEEP_STATE) |
|||
sleep_colortemp = config.get(CONF_SLEEP_CT) |
|||
sleep_brightness = config.get(CONF_SLEEP_BRIGHT) |
|||
disable_entity = config.get(CONF_DISABLE_ENTITY) |
|||
disable_state = config.get(CONF_DISABLE_STATE) |
|||
initial_transition = config.get(CONF_INITIAL_TRANSITION) |
|||
cs = CircadianSwitch(hass, cl, name, lights_ct, lights_rgb, lights_xy, lights_brightness, |
|||
disable_brightness_adjust, min_brightness, max_brightness, |
|||
sleep_entity, sleep_state, sleep_colortemp, sleep_brightness, |
|||
disable_entity, disable_state, initial_transition) |
|||
add_devices([cs]) |
|||
|
|||
def update(call=None): |
|||
"""Update lights.""" |
|||
cs.update_switch() |
|||
return True |
|||
else: |
|||
return False |
|||
|
|||
|
|||
class CircadianSwitch(SwitchEntity, RestoreEntity): |
|||
"""Representation of a Circadian Lighting switch.""" |
|||
|
|||
def __init__(self, hass, cl, name, lights_ct, lights_rgb, lights_xy, lights_brightness, |
|||
disable_brightness_adjust, min_brightness, max_brightness, |
|||
sleep_entity, sleep_state, sleep_colortemp, sleep_brightness, |
|||
disable_entity, disable_state, initial_transition): |
|||
"""Initialize the Circadian Lighting switch.""" |
|||
self.hass = hass |
|||
self._cl = cl |
|||
self._name = name |
|||
self._entity_id = "switch." + slugify("{} {}".format('circadian_lighting', name)) |
|||
self._state = None |
|||
self._icon = ICON |
|||
self._hs_color = None |
|||
self._lights_ct = lights_ct |
|||
self._lights_rgb = lights_rgb |
|||
self._lights_xy = lights_xy |
|||
self._lights_brightness = lights_brightness |
|||
self._disable_brightness_adjust = disable_brightness_adjust |
|||
self._min_brightness = min_brightness |
|||
self._max_brightness = max_brightness |
|||
self._sleep_entity = sleep_entity |
|||
self._sleep_state = sleep_state |
|||
self._sleep_colortemp = sleep_colortemp |
|||
self._sleep_brightness = sleep_brightness |
|||
self._disable_entity = disable_entity |
|||
self._disable_state = disable_state |
|||
self._initial_transition = initial_transition |
|||
self._attributes = {} |
|||
self._attributes['hs_color'] = self._hs_color |
|||
self._attributes['brightness'] = None |
|||
|
|||
self._lights = [] |
|||
if lights_ct != None: |
|||
self._lights += lights_ct |
|||
if lights_rgb != None: |
|||
self._lights += lights_rgb |
|||
if lights_xy != None: |
|||
self._lights += lights_xy |
|||
if lights_brightness != None: |
|||
self._lights += lights_brightness |
|||
|
|||
"""Register callbacks.""" |
|||
dispatcher_connect(hass, CIRCADIAN_LIGHTING_UPDATE_TOPIC, self.update_switch) |
|||
track_state_change(hass, self._lights, self.light_state_changed) |
|||
if self._sleep_entity is not None: |
|||
track_state_change(hass, self._sleep_entity, self.sleep_state_changed) |
|||
if self._disable_entity is not None: |
|||
track_state_change(hass, self._disable_entity, self.disable_state_changed) |
|||
|
|||
@property |
|||
def entity_id(self): |
|||
"""Return the entity ID of the switch.""" |
|||
return self._entity_id |
|||
|
|||
@property |
|||
def name(self): |
|||
"""Return the name of the device if any.""" |
|||
return self._name |
|||
|
|||
@property |
|||
def is_on(self): |
|||
"""Return true if circadian lighting is on.""" |
|||
return self._state |
|||
|
|||
async def async_added_to_hass(self): |
|||
"""Call when entity about to be added to hass.""" |
|||
# If not None, we got an initial value. |
|||
await super().async_added_to_hass() |
|||
if self._state is not None: |
|||
return |
|||
|
|||
state = await self.async_get_last_state() |
|||
self._state = state and state.state == STATE_ON |
|||
|
|||
@property |
|||
def icon(self): |
|||
"""Icon to use in the frontend, if any.""" |
|||
return self._icon |
|||
|
|||
@property |
|||
def hs_color(self): |
|||
return self._hs_color |
|||
|
|||
@property |
|||
def device_state_attributes(self): |
|||
"""Return the attributes of the switch.""" |
|||
return self._attributes |
|||
|
|||
def turn_on(self, **kwargs): |
|||
"""Turn on circadian lighting.""" |
|||
self._state = True |
|||
|
|||
# Make initial update |
|||
self.update_switch(self._initial_transition) |
|||
|
|||
self.schedule_update_ha_state() |
|||
|
|||
def turn_off(self, **kwargs): |
|||
"""Turn off circadian lighting.""" |
|||
self._state = False |
|||
self.schedule_update_ha_state() |
|||
self._hs_color = None |
|||
self._attributes['hs_color'] = self._hs_color |
|||
self._attributes['brightness'] = None |
|||
|
|||
def is_sleep(self): |
|||
return self._sleep_entity is not None and self.hass.states.get(self._sleep_entity).state == self._sleep_state |
|||
|
|||
def calc_ct(self): |
|||
if self.is_sleep(): |
|||
_LOGGER.debug(self._name + " in Sleep mode") |
|||
return color_temperature_kelvin_to_mired(self._sleep_colortemp) |
|||
else: |
|||
return color_temperature_kelvin_to_mired(self._cl.data['colortemp']) |
|||
|
|||
def calc_rgb(self): |
|||
if self.is_sleep(): |
|||
_LOGGER.debug(self._name + " in Sleep mode") |
|||
return color_temperature_to_rgb(self._sleep_colortemp) |
|||
else: |
|||
return color_temperature_to_rgb(self._cl.data['colortemp']) |
|||
|
|||
def calc_xy(self): |
|||
return color_RGB_to_xy(*self.calc_rgb()) |
|||
|
|||
def calc_hs(self): |
|||
return color_xy_to_hs(*self.calc_xy()) |
|||
|
|||
def calc_brightness(self): |
|||
if self._disable_brightness_adjust is True: |
|||
return None |
|||
else: |
|||
if self.is_sleep(): |
|||
_LOGGER.debug(self._name + " in Sleep mode") |
|||
return self._sleep_brightness |
|||
else: |
|||
if self._cl.data['percent'] > 0: |
|||
return self._max_brightness |
|||
else: |
|||
return ((self._max_brightness - self._min_brightness) * ((100+self._cl.data['percent']) / 100)) + self._min_brightness |
|||
|
|||
def update_switch(self, transition=None): |
|||
if self._cl.data is not None: |
|||
self._hs_color = self.calc_hs() |
|||
self._attributes['hs_color'] = self._hs_color |
|||
self._attributes['brightness'] = self.calc_brightness() |
|||
_LOGGER.debug(self._name + " Switch Updated") |
|||
|
|||
self.adjust_lights(self._lights, transition) |
|||
|
|||
def should_adjust(self): |
|||
if self._state is not True: |
|||
_LOGGER.debug(self._name + " off - not adjusting") |
|||
return False |
|||
elif self._cl.data is None: |
|||
_LOGGER.debug(self._name + " could not retrieve Circadian Lighting data") |
|||
return False |
|||
elif self._disable_entity is not None and self.hass.states.get(self._disable_entity).state == self._disable_state: |
|||
_LOGGER.debug(self._name + " disabled by " + str(self._disable_entity)) |
|||
return False |
|||
else: |
|||
return True |
|||
|
|||
def adjust_lights(self, lights, transition=None): |
|||
if self.should_adjust(): |
|||
if transition == None: |
|||
transition = self._cl.data['transition'] |
|||
|
|||
brightness = int((self._attributes['brightness'] / 100) * 254) if self._attributes['brightness'] is not None else None |
|||
mired = int(self.calc_ct()) if self._lights_ct is not None else None |
|||
rgb = tuple(map(int, self.calc_rgb())) if self._lights_rgb is not None else None |
|||
xy = self.calc_xy() if self._lights_xy is not None else None |
|||
|
|||
for light in lights: |
|||
"""Set color of array of ct light if on.""" |
|||
if self._lights_ct is not None and light in self._lights_ct and is_on(self.hass, light): |
|||
service_data = {ATTR_ENTITY_ID: light} |
|||
if mired is not None: |
|||
service_data[ATTR_COLOR_TEMP] = mired |
|||
if brightness is not None: |
|||
service_data[ATTR_BRIGHTNESS] = brightness |
|||
if transition is not None: |
|||
service_data[ATTR_TRANSITION] = transition |
|||
self.hass.services.call( |
|||
LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) |
|||
_LOGGER.debug(light + " CT Adjusted - color_temp: " + str(mired) + ", brightness: " + str(brightness) + ", transition: " + str(transition)) |
|||
|
|||
"""Set color of array of rgb light if on.""" |
|||
if self._lights_rgb is not None and light in self._lights_rgb and is_on(self.hass, light): |
|||
service_data = {ATTR_ENTITY_ID: light} |
|||
if rgb is not None: |
|||
service_data[ATTR_RGB_COLOR] = rgb |
|||
if brightness is not None: |
|||
service_data[ATTR_BRIGHTNESS] = brightness |
|||
if transition is not None: |
|||
service_data[ATTR_TRANSITION] = transition |
|||
self.hass.services.call( |
|||
LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) |
|||
_LOGGER.debug(light + " RGB Adjusted - rgb_color: " + str(rgb) + ", brightness: " + str(brightness) + ", transition: " + str(transition)) |
|||
|
|||
"""Set color of array of xy light if on.""" |
|||
if self._lights_xy is not None and light in self._lights_xy and is_on(self.hass, light): |
|||
service_data = {ATTR_ENTITY_ID: light} |
|||
if xy is not None: |
|||
service_data[ATTR_XY_COLOR] = xy |
|||
if brightness is not None: |
|||
service_data[ATTR_BRIGHTNESS] = brightness |
|||
service_data[ATTR_WHITE_VALUE] = brightness |
|||
if transition is not None: |
|||
service_data[ATTR_TRANSITION] = transition |
|||
self.hass.services.call( |
|||
LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) |
|||
_LOGGER.debug(light + " XY Adjusted - xy_color: " + str(xy) + ", brightness: " + str(brightness) + ", transition: " + str(transition) + ", white_value: " + str(brightness)) |
|||
|
|||
"""Set color of array of brightness light if on.""" |
|||
if self._lights_brightness is not None and light in self._lights_brightness and is_on(self.hass, light): |
|||
service_data = {ATTR_ENTITY_ID: light} |
|||
if brightness is not None: |
|||
service_data[ATTR_BRIGHTNESS] = brightness |
|||
if transition is not None: |
|||
service_data[ATTR_TRANSITION] = transition |
|||
self.hass.services.call( |
|||
LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) |
|||
_LOGGER.debug(light + " Brightness Adjusted - brightness: " + str(brightness) + ", transition: " + str(transition)) |
|||
|
|||
def light_state_changed(self, entity_id, from_state, to_state): |
|||
try: |
|||
_LOGGER.debug(entity_id + " change from " + str(from_state) + " to " + str(to_state)) |
|||
if to_state.state == 'on' and from_state.state != 'on': |
|||
self.adjust_lights([entity_id], self._initial_transition) |
|||
except: |
|||
pass |
|||
|
|||
def sleep_state_changed(self, entity_id, from_state, to_state): |
|||
try: |
|||
_LOGGER.debug(entity_id + " change from " + str(from_state) + " to " + str(to_state)) |
|||
if to_state.state == self._sleep_state or from_state.state == self._sleep_state: |
|||
self.update_switch(self._initial_transition) |
|||
except: |
|||
pass |
|||
|
|||
def disable_state_changed(self, entity_id, from_state, to_state): |
|||
try: |
|||
_LOGGER.debug(entity_id + " change from " + str(from_state) + " to " + str(to_state)) |
|||
if from_state.state == self._disable_state: |
|||
self.update_switch(self._initial_transition) |
|||
except: |
|||
pass |
@ -0,0 +1 @@ |
|||
db_url: !secret recorder_db_url |
Reference in new issue