Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Philips Hue

philips-hue.livemd

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(&amp;Helper.display_body/1)

Get devices bound to the bridge

HttpClientV2.get("/resource/device")
|> then(&amp;Helper.display_body/1)

Get lamp configuration

lamp =
  HttpClientV2.get("/resource/light/9703b59c-1675-4d45-a2f3-164f827d377b")
  |> then(&amp;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(&amp;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(&amp;Helper.display_body/1)