Philips Hue
Mix.install([
{:kino, "~> 0.12.3"},
{:tesla, "~> 1.9"},
{:jason, "~> 1.4"},
{:finch, "~> 0.18.0"}
])
Modules
defmodule Helper do
def display_body({:ok, %Tesla.Env{headers: headers} = response}) do
%Tesla.Env{body: x} = response
local_headers = Map.new(headers)
case Map.fetch(local_headers, "content-type") do
{:ok, "application/json"} ->
x
{:ok, "text/html"} ->
Kino.HTML.new(x)
end
end
def display_body({:error, response}) do
response
end
end
# https://developers.meethue.com/develop/application-design-guidance/color-conversion-formulas-rgb-to-xy-and-back/
defmodule Gradient do
def get(100) do
[
rgb_to_xy("00FF00"),
rgb_to_xy("00FF00"),
rgb_to_xy("00FF00"),
rgb_to_xy("00FF00")
]
end
def get(level)
when is_integer(level) and
level >= 75 do
[
rgb_to_xy("00FF00"),
rgb_to_xy("00FF00"),
rgb_to_xy("00FF00"),
hsv_to_xy(120, remaining_pixel_saturation(level), 100)
]
end
def get(level)
when is_integer(level) and
level >= 50 do
[
rgb_to_xy("00FF00"),
rgb_to_xy("00FF00"),
hsv_to_xy(120, remaining_pixel_saturation(level), 100),
rgb_to_xy("FFFFFF")
]
end
def get(level)
when is_integer(level) and
level >= 25 do
[
rgb_to_xy("00FF00"),
hsv_to_xy(120, remaining_pixel_saturation(level), 100),
rgb_to_xy("FFFFFF"),
rgb_to_xy("FFFFFF")
]
end
def get(level) when is_integer(level) do
[
hsv_to_xy(120, remaining_pixel_saturation(level), 100),
rgb_to_xy("FFFFFF"),
rgb_to_xy("FFFFFF"),
rgb_to_xy("FFFFFF")
]
end
defp remaining_pixel_saturation(percentage)
when is_integer(percentage) and percentage in 0..100 do
remaining_value = rem(percentage, 25)
trunc(remaining_value * 100 / 25)
end
def rgb_to_xy(
<> <>
<> <>
<>
) do
{x, y, z} =
rgb_to_xyz(
String.to_integer(red, 16),
String.to_integer(green, 16),
String.to_integer(blue, 16)
)
local_x = x / (x + y + z)
local_y = y / (x + y + z)
{local_x, local_y}
end
def hsv_to_xy(h, s, v)
when h in 0..360 and
s in 0..100 and
v in 0..100 do
{r, g, b} = hsv_to_rgb(h, s, v) |> IO.inspect(label: "hsv_to_rgb")
{x, y, z} = rgb_to_xyz(r, g, b) |> IO.inspect(label: "rgb_to_xyz")
local_x = x / (x + y + z)
local_y = y / (x + y + z)
{local_x, local_y}
end
defp hsv_to_rgb(h, s, v)
when h in 0..360 and
s in 0..100 and
v in 0..100 do
local_s = s / 100
local_v = v / 100
c = local_s * local_v
x = c * (1 - abs(:math.fmod(h / 60, 2) - 1))
m = local_v - c
IO.inspect(c, label: "hsv_to_rgb: c #{c}")
IO.inspect(x, label: "hsv_to_rgb: x #{x}")
IO.inspect(m, label: "hsv_to_rgb: m #{m}")
{r, g, b} =
case h do
y when y >= 0 and y < 60 -> {c, x, 0}
y when y >= 60 and y < 120 -> {x, c, 0}
y when y >= 120 and y < 180 -> {0, c, x}
y when y >= 180 and y < 240 -> {0, x, c}
y when y >= 240 and y < 300 -> {x, 0, c}
_ -> {c, 0, x}
end
{r + m, g + m, b + m}
end
defp rgb_to_xyz(red, green, blue)
when is_float(red) and red >= 0 and red <= 1 and
is_float(green) and green >= 0 and green <= 1 and
is_float(blue) and blue >= 0 and blue <= 1 do
local_red =
red
|> apply_gamma_correction()
local_green =
green
|> apply_gamma_correction()
local_blue =
blue
|> apply_gamma_correction()
x = local_red * 0.4124 + local_green * 0.3576 + local_blue * 0.1805
y = local_red * 0.2126 + local_green * 0.7152 + local_blue * 0.0722
z = local_red * 0.0193 + local_green * 0.1195 + local_blue * 0.9504
{x, y, z}
end
defp rgb_to_xyz(red, green, blue)
when is_integer(red) and red in 0..255 and
is_integer(green) and green in 0..255 and
is_integer(blue) and blue in 0..255 do
local_red =
red
|> translate_to_1()
local_green =
green
|> translate_to_1()
local_blue =
blue
|> translate_to_1()
rgb_to_xyz(local_red, local_green, local_blue)
end
defp translate_to_1(value)
when value in 0..255,
do: value / 255
defp apply_gamma_correction(value)
when value >= 0.0 and value <= 1.0 and value > 0.04045,
do: Float.pow((value + 0.55) / 1.055, 2.4)
defp apply_gamma_correction(value)
when value >= 0.0 and value <= 1.0,
do: value / 12.92
end
defmodule BasicHttpClient do
use Tesla
adapter(Tesla.Adapter.Finch, name: LiveBookFinch)
plug(Tesla.Middleware.BaseUrl, "https://192.168.1.189")
plug(Tesla.Middleware.Logger)
plug(Tesla.Middleware.JSON)
end
defmodule HttpClientV2 do
use Tesla
adapter(Tesla.Adapter.Finch, name: LiveBookFinch)
plug(Tesla.Middleware.BaseUrl, "https://192.168.1.189/clip/v2")
plug(Tesla.Middleware.Logger)
plug(Tesla.Middleware.Headers, [
{"hue-application-key", "LlAFoXtPKZfsc2fGBbeNVD2dYc5-5nbsjTAsmbV0"},
{"Content-Type", "application/json"}
])
plug(Tesla.Middleware.JSON)
end
# https://developers.meethue.com/develop/application-design-guidance/color-conversion-formulas-rgb-to-xy-and-back/
defmodule Colors do
def red() do
{x, y} = Gradient.rgb_to_xy("FF0000")
%{xy: %{x: x, y: y}}
end
def green() do
{x, y} = Gradient.rgb_to_xy("00FF00")
%{xy: %{x: x, y: y}}
end
def blue() do
{x, y} = Gradient.rgb_to_xy("0000FF")
%{xy: %{x: x, y: y}}
end
def white() do
{x, y} = Gradient.rgb_to_xy("FFFFFF")
%{xy: %{x: x, y: y}}
end
def yellow() do
{x, y} = Gradient.rgb_to_xy("FFFF00")
%{xy: %{x: x, y: y}}
end
def orange() do
{x, y} = Gradient.rgb_to_xy("FF8000")
%{xy: %{x: x, y: y}}
end
end
defmodule HttpClient do
use Tesla
adapter(Tesla.Adapter.Finch, name: LiveBookFinch)
plug(
Tesla.Middleware.BaseUrl,
"http://192.168.1.189/api/zGy79yLSBfamDB4VbQcXzjRGxUkZrVsNlqgVBzNQ"
)
# plug(Tesla.Middleware.Logger)
plug(Tesla.Middleware.JSON)
plug(Tesla.Middleware.Retry,
delay: 500,
max_retries: 10,
max_delay: 4_000,
should_retry: fn
{:ok, %{status: status}} when status in [500] -> true
{:ok, _} -> false
{:error, _} -> true
end
)
end
Kino.start_child!(
{Finch,
name: LiveBookFinch,
pools: %{
default: [
conn_opts: [
transport_opts: [
verify: :verify_peer,
verify_fun: {fn _, _, state -> {:valid, state} end, []}
]
]
]
}}
)
Bridge authentication
Hue V2 API, getting started: https://developers.meethue.com/develop/hue-api-v2/getting-started/
BasicHttpClient.post("https://192.168.1.189/api", %{
devicetype: "live-book",
generateclientkey: true
})
|> then(&Helper.display_body/1)
Get devices bound to the bridge
HttpClientV2.get("/resource/device")
|> then(&Helper.display_body/1)
Get lamp configuration
lamp =
HttpClientV2.get("/resource/light/9703b59c-1675-4d45-a2f3-164f827d377b")
|> then(&Helper.display_body/1)
Get current gradient points
%{"data" => first_data} = lamp
[head | _] = first_data
%{"gradient" => gradient} = head
gradient
Set to green, red, blue and white
HttpClientV2.put(
"/resource/light/9703b59c-1675-4d45-a2f3-164f827d377b",
%{
gradient: %{
points: [
%{color: Colors.green()},
%{color: Colors.red()},
%{color: Colors.blue()},
%{color: Colors.orange()}
]
}
}
)
|> then(&Helper.display_body/1)
Set to 91% level
points =
Gradient.get(91)
|> Enum.map(fn {x, y} -> %{color: %{xy: %{x: x, y: y}}} end)
HttpClientV2.put(
"/resource/light/9703b59c-1675-4d45-a2f3-164f827d377b",
%{
gradient: %{
points: points
}
}
)
|> then(&Helper.display_body/1)