tasmota finder + mix fmt.

664b307 · gmorell · 2023-09-22 02:29

17 files +264 -29

Files changed

modified Dockerfile
+1 −1
@@ -1,4 +1,4 @@
1 −FROM elixir:alpine
1 +FROM elixir:1.14.5-otp-25-alpine
2 2 MAINTAINER Gabe <gmp@gmp.io>
3 3 ARG app_name=lambent_ex
4 4 ARG phoenix_subdir=.
modified lib/lambent_ex/meta.ex
+2 −2
@@ -194,9 +194,9 @@ defmodule LambentEx.Meta do
194 194 def handle_info(:svc_pub, state) do
195 195 Process.send_after(self(), :svc_pub, 500)
196 196
197 − Phoenix.PubSub.broadcast(@pubsub_name, @pubsub_svc, {:http, get_http})
197 + Phoenix.PubSub.broadcast(@pubsub_name, @pubsub_svc, {:http, get_http()})
198 198 # Phoenix.PubSub.broadcast(@pubsub_name, @pubsub_svc, {:mqtt, get_mqtt})
199 − Phoenix.PubSub.broadcast(@pubsub_name, @pubsub_svc, {:cronos, get_cronos})
199 + Phoenix.PubSub.broadcast(@pubsub_name, @pubsub_svc, {:cronos, get_cronos()})
200 200 {:noreply, state}
201 201 end
202 202
modified lib/lambent_ex/mqtt.ex
+208 −4
@@ -1,10 +1,20 @@
1 1 defmodule LambentEx.MQTT do
2 2 @moduledoc false
3 3 use GenServer
4 + require Logger
5 +
6 + alias LambentEx.Meta, as: M
7 +
4 8 @registry :lambent_mqtt
5 9 @pubsub_name LambentEx.PubSub
6 10 @pubsub_topic_idx "svc_idx"
7 11
12 + @mqtt_topic_prefix_tasmota_tele "tele/"
13 + @mqtt_topic_prefix_tasmota_cmnd "cmnd/"
14 + @mqtt_topic_prefix_tasmota_stat "stat/"
15 +
16 + @lighting_keywords ["Color", "Fade", "White", "CT", "Dimmer"]
17 +
8 18 def start_link(_args, opts) do
9 19 GenServer.start_link(__MODULE__, opts, name: via_tuple("mqtt-#{opts[:name]}"))
10 20 end
@@ -12,15 +22,27 @@ defmodule LambentEx.MQTT do
12 22 def init(opts) do
13 23 Process.send_after(self(), :publish, 50)
14 24
15 − {:ok, opts} = Keyword.validate(opts, [:name, :host, :port, tls: false, persist: false])
25 + {:ok, opts} =
26 + Keyword.validate(opts, [:name, :host, :port, tls: false, persist: false, modes: [:tasmota]])
16 27
17 28 {:ok, pid} =
18 29 Tortoise.Connection.start_link(
19 30 client_id: opts[:name],
20 31 server: {Tortoise.Transport.Tcp, host: opts[:host], port: opts[:port]},
21 − handler: {Tortoise.Handler.Logger, []}
32 + handler: {LambentEx.MQTT.Handler, [id: self()]}
22 33 )
23 34
35 + cond do
36 + Enum.member?(opts[:modes], :tasmota) ->
37 + Tortoise.Connection.subscribe(opts[:name], @mqtt_topic_prefix_tasmota_tele <> "#", qos: 0)
38 + Tortoise.Connection.subscribe(opts[:name], @mqtt_topic_prefix_tasmota_stat <> "#", qos: 0)
39 + Logger.info("Subscribed to tasmota subscriptions")
40 + :ok
41 +
42 + Enum.member?(opts[:modes], :apollos_crib) ->
43 + :ok
44 + end
45 +
24 46 Tortoise.Events.register(opts[:name], :status)
25 47 Tortoise.Events.register(opts[:name], :ping_response)
26 48
@@ -32,19 +54,46 @@ defmodule LambentEx.MQTT do
32 54 port: opts[:port],
33 55 persist: opts[:persist],
34 56 connected: :down,
35 − latency: nil
57 + latency: nil,
58 + modes: opts[:modes],
59 + d_sensors: %{},
60 + d_lights: %{}
36 61 }}
37 62 end
38 63
39 64 defp via_tuple(name), do: {:via, Registry, {@registry, name}}
40 65
41 66 # public apis
67 + def do_poke(name, id) do
68 + via_tuple("mqtt-#{name}")
69 + |> GenServer.cast({:poke, id})
70 + end
71 +
72 + def rename(name, id, new) do
73 + via_tuple("mqtt-#{name}")
74 + |> GenServer.cast({:rename_mac, id, new})
75 + end
76 +
77 + def replace(name, id, new) do
78 + via_tuple("mqtt-#{name}")
79 + |> GenServer.cast({:replace, id, new})
80 + end
42 81
43 82 # callbacks
83 + defp add_type(devices, name) do
84 + devices |> Enum.map(fn {k, v} -> {k, v |> Map.put("type", "mqtt-tasmota") |> Map.put("dname", name)} end) |> Map.new()
85 + end
86 +
44 87 @impl true
45 88 def handle_info(:publish, state) do
46 89 Process.send_after(self(), :publish, 400)
47 90 Phoenix.PubSub.broadcast(@pubsub_name, @pubsub_topic_idx, {:mqtt_pub, publish(state)})
91 +
92 + Phoenix.PubSub.broadcast(
93 + @pubsub_name,
94 + "scan-82667777",
95 + {:devices_pub, state[:d_lights] |> add_type(state[:name])}
96 + )
48 97 {:noreply, state}
49 98 end
50 99
@@ -59,6 +108,44 @@ defmodule LambentEx.MQTT do
59 108 end
60 109
61 110 # impls
111 + def handle_info({{Tortoise, "hass"}, ref, result}, state) do
112 + IO.inspect({"gottem", ref, result})
113 + {:noreply, state}
114 + end
115 +
116 + def handle_info({:light, device, payload}, state) do
117 + c = state[:d_lights]
118 + d = c |> Map.get(device, %{})
119 + m = Map.merge(d, payload)
120 + n = M.get_name(device)
121 + p = M.get_place(device)
122 + c2 = c |> Map.put(device, m |> Map.put("name", n) |> Map.put("place", p))
123 + {:noreply, state |> Map.put(:d_lights, c2)}
124 + end
125 +
126 + @impl true
127 + def handle_cast({:poke, id}, state) do
128 + IO.inspect("got poke")
129 + {:noreply, state}
130 + end
131 +
132 + @impl true
133 + def handle_cast({:rename_mac, device, name}, state) do
134 + d = state[:d_lights] |> Map.get(device, %{})
135 + dev = state[:d_lights] |> Map.put(device, d |> Map.put("name", name))
136 +
137 + M.put_name(device, name)
138 + {:noreply, %{state | d_lights: dev}}
139 + end
140 +
141 + @impl true
142 + def handle_cast({:replace, device, place}, state) do
143 + d = state[:d_lights] |> Map.get(device, %{})
144 + dev = state[:d_lights] |> Map.put(device, d |> Map.put("place", place))
145 +
146 + M.put_place(device, place)
147 + {:noreply, %{state | d_lights: dev}}
148 + end
62 149
63 150 defp publish(state) do
64 151 %{
@@ -67,7 +154,124 @@ defmodule LambentEx.MQTT do
67 154 port: state[:port],
68 155 persist: state[:persist],
69 156 connected: state[:connected],
70 − latency: state[:latency]
157 + latency: state[:latency],
158 + d_lights: state[:d_lights],
159 + d_sensors: state[:d_sensors]
71 160 }
72 161 end
73 162 end
163 +
164 +defmodule LambentEx.MQTT.Handler do
165 + @moduledoc false
166 +
167 + require Logger
168 +
169 + use Tortoise.Handler
170 +
171 + defstruct []
172 + alias __MODULE__, as: State
173 +
174 + def init(opts) do
175 + Logger.info("Initializing handler")
176 + {:ok, %{id: opts[:id]}}
177 + end
178 +
179 + def connection(:up, state) do
180 + Logger.info("Connection has been established")
181 + {:ok, state}
182 + end
183 +
184 + def connection(:down, state) do
185 + Logger.warn("Connection has been dropped")
186 + {:ok, state}
187 + end
188 +
189 + def connection(:terminating, state) do
190 + Logger.warn("Connection is terminating")
191 + {:ok, state}
192 + end
193 +
194 + def subscription(:up, topic, state) do
195 + Logger.info("Subscribed to #{topic}")
196 + {:ok, state}
197 + end
198 +
199 + def subscription({:warn, [requested: req, accepted: qos]}, topic, state) do
200 + Logger.warn("Subscribed to #{topic}; requested #{req} but got accepted with QoS #{qos}")
201 + {:ok, state}
202 + end
203 +
204 + def subscription({:error, reason}, topic, state) do
205 + Logger.error("Error subscribing to #{topic}; #{inspect(reason)}")
206 + {:ok, state}
207 + end
208 +
209 + def subscription(:down, topic, state) do
210 + Logger.info("Unsubscribed from #{topic}")
211 + {:ok, state}
212 + end
213 +
214 + def publish_entity(pid, cls, device, payload) do
215 + Process.send(pid, {cls, device, payload}, [])
216 + end
217 +
218 + def handle_message(topic, payload, state) do
219 + case topic do
220 + ["tele", device, "LWT"] ->
221 + Logger.info("#{device}: got LWT, queueing discovery work, #{payload}")
222 +
223 + ["tele", device, "STATE"] ->
224 + Logger.info("#{device}: got STATE, updating device states, #{payload}")
225 + {:ok, p} = Poison.decode(payload)
226 +
227 + if is_lighting_entity?(:p, p) do
228 + publish_entity(state[:id], :light, device, p)
229 + end
230 +
231 + ["tele", device, "INFO1"] ->
232 + Logger.info("#{device}: got INFO1, updating device states, #{payload}")
233 +
234 + ["tele", device, "INFO2"] ->
235 + Logger.info("#{device}: got INFO2, updating device states, #{payload}")
236 +
237 + ["tele", device, "INFO3"] ->
238 + Logger.info("#{device}: got INFO3, updating device states, #{payload}")
239 +
240 + ["stat", device, "POWER"] ->
241 + Logger.info("#{device}: got POWER message, #{payload}")
242 +
243 + ["stat", device, "RESULT"] ->
244 + Logger.info("#{device}: got RESULT message, #{payload}")
245 + {:ok, p} = Poison.decode(payload)
246 +
247 + if is_lighting_entity?(:p, p) do
248 + publish_entity(state[:id], :light, device, p)
249 + end
250 + _otherwise ->
251 + Logger.debug("xxx---#{Enum.join(topic, "/")} #{inspect(payload)}")
252 + end
253 +
254 + {:ok, state}
255 + end
256 +
257 + def terminate(reason, _state) do
258 + Logger.warn("Client has been terminated with reason: #{inspect(reason)}")
259 + :ok
260 + end
261 +
262 + def is_lighting_entity?(:t, topic) do
263 + "tasmota" in topic and
264 + Enum.any?(@lighting_keywords, fn keyword -> String.member?(topic, keyword) end)
265 + end
266 +
267 + def is_lighting_entity?(:p, payload) do
268 + keys = payload |> Map.keys()
269 +
270 + Enum.any?(["Color", "Fade", "White", "CT", "Dimmer"], fn keyword ->
271 + Enum.member?(keys, keyword)
272 + end)
273 + end
274 +
275 + def is_sensor_entity(:p, payload) do
276 + end
277 +end
modified lib/lambent_ex/util/color.ex
+7 −7
@@ -122,13 +122,13 @@ defmodule LambentEx.Utils.Color do
122 122 def cy, do: [192, 192, 0]
123 123 def ck, do: [0, 0, 0]
124 124
125 − def cre, do: cr |> Stream.cycle() |> Enum.take(900)
126 − def cge, do: cg |> Stream.cycle() |> Enum.take(900)
127 − def cbe, do: cb |> Stream.cycle() |> Enum.take(900)
128 − def cce, do: cc |> Stream.cycle() |> Enum.take(900)
129 − def cme, do: cm |> Stream.cycle() |> Enum.take(900)
130 − def cye, do: cy |> Stream.cycle() |> Enum.take(900)
131 − def cke, do: ck |> Stream.cycle() |> Enum.take(900)
125 + def cre, do: cr() |> Stream.cycle() |> Enum.take(900)
126 + def cge, do: cg() |> Stream.cycle() |> Enum.take(900)
127 + def cbe, do: cb() |> Stream.cycle() |> Enum.take(900)
128 + def cce, do: cc() |> Stream.cycle() |> Enum.take(900)
129 + def cme, do: cm() |> Stream.cycle() |> Enum.take(900)
130 + def cye, do: cy() |> Stream.cycle() |> Enum.take(900)
131 + def cke, do: ck() |> Stream.cycle() |> Enum.take(900)
132 132
133 133 # misc
134 134 def hex(val) when is_float(val), do: trunc(val) |> hex
modified lib/lambent_ex_web/live/devices/index.ex
+36 −7
@@ -27,16 +27,22 @@ defmodule LambentExWeb.DevicesLive.Index do
27 27
28 28 @impl true
29 29 def handle_info({:devices_pub, devices}, socket) do
30 − {:noreply, socket |> assign(:devices, devices)}
30 + n = Map.merge(socket.assigns.devices, devices)
31 + {:noreply, socket |> assign(:devices, n)}
31 32 end
32 33
33 34 @impl true
34 35 def handle_info({:rename, data}, socket) do
35 36 id = data["id"] |> String.trim_trailing("name") |> String.trim_trailing("namem")
37 + name = data["data"] |> re_nil
36 38
37 39 case socket.assigns[:devices] |> Map.get(id) |> Map.get("type") do
38 40 "8266-7777" ->
39 − LambentEx.Scan.ESP8266x7777.rename(:mac, id, data["data"] |> re_nil)
41 + LambentEx.Scan.ESP8266x7777.rename(:mac, id, name)
42 +
43 + "mqtt-tasmota" ->
44 + xname = socket.assigns[:devices] |> Map.get(id) |> Map.get("dname")
45 + LambentEx.MQTT.rename(xname, id, name)
40 46
41 47 type ->
42 48 Logger.info("Need handler for this type: #{type}")
@@ -66,11 +72,15 @@ defmodule LambentExWeb.DevicesLive.Index do
66 72 @impl true
67 73 def handle_info({:replace, data}, socket) do
68 74 id = data["id"] |> String.trim_trailing("place") |> String.trim_trailing("placem")
69 − IO.inspect(id)
75 + pl = data["data"] |> re_nil
70 76
71 77 case socket.assigns[:devices] |> Map.get(id) |> Map.get("type") do
72 78 "8266-7777" ->
73 − LambentEx.Scan.ESP8266x7777.replace(:mac, id, data["data"] |> re_nil)
79 + LambentEx.Scan.ESP8266x7777.replace(:mac, id, pl)
80 +
81 + "mqtt-tasmota" ->
82 + xname = socket.assigns[:devices] |> Map.get(id) |> Map.get("dname")
83 + LambentEx.MQTT.replace(xname, id, pl)
74 84
75 85 type ->
76 86 Logger.info("Need handler for this type: #{type}")
@@ -81,11 +91,26 @@ defmodule LambentExWeb.DevicesLive.Index do
81 91 end
82 92
83 93 @impl true
84 − def handle_event("poke", %{"id" => id}, socket) do
94 + def handle_event("poke", %{"id" => id, "dname" => name}, socket) do
85 95 case socket.assigns[:devices] |> Map.get(id) |> Map.get("type") do
86 96 "8266-7777" ->
87 97 LambentEx.Scan.ESP8266x7777.poke(id)
98 + "mqtt-tasmota" ->
99 + LambentEx.MQTT.do_poke(name, id)
100 +
101 + type ->
102 + Logger.info("Need handler for this type: #{type}")
103 + :ok
104 + end
105 +
106 + {:noreply, socket}
107 + end
88 108
109 + @impl true
110 + def handle_event("poke", %{"id" => id}, socket) do
111 + case socket.assigns[:devices] |> Map.get(id) |> Map.get("type") do
112 + "8266-7777" ->
113 + LambentEx.Scan.ESP8266x7777.poke(id)
89 114 type ->
90 115 Logger.info("Need handler for this type: #{type}")
91 116 :ok
@@ -101,8 +126,12 @@ defmodule LambentExWeb.DevicesLive.Index do
101 126 |> assign(:source, nil)
102 127 end
103 128
104 − defp pip(ip) do
105 − ip |> Tuple.to_list() |> Enum.join(".")
129 + defp pip(map) do
130 + cond do
131 + Map.has_key?(map, "ip") -> Map.get(map, "ip") |> Tuple.to_list() |> Enum.join(".")
132 + Map.has_key?(map, "POWER") -> "MQTT"
133 + end
134 +
106 135 end
107 136
108 137 defp options() do
modified lib/lambent_ex_web/live/devices/index.html.heex
+3 −3
@@ -23,7 +23,7 @@
23 23 <%= for {k,v} <- @devices do %>
24 24 <tr>
25 25 <th><%= k %></th>
26 − <td><%= v["ip"] |> pip %></td>
26 + <td><%= v |> pip %></td>
27 27 <td>
28 28 <.live_editable
29 29 id={k <> "name"}
@@ -52,7 +52,7 @@
52 52 </td>
53 53 <td><%= v["type"] %></td>
54 54 <td>
55 − <button class="btn btn-circle" phx-click="poke" phx-value-id={k}>
55 + <button class="btn btn-circle" phx-click="poke" phx-value-id={k} phx-value-dname={v["dname"]}>
56 56 <i class="fa-solid fa-bolt-lightning"></i>
57 57 </button>
58 58 </td>
@@ -91,7 +91,7 @@
91 91 />
92 92 </h2>
93 93 <p>
94 − <%= v["ip"] |> pip %> <i class="fa-solid fa-at"></i> <%= k %>
94 + <%= v |> pip %> <i class="fa-solid fa-at"></i> <%= k %>
95 95
96 96 <div class="badge badge-lg badge-secondary">
97 97 <i class="fa-solid fa-fw fa-traffic-light fa-spin"></i>
modified lib/lambent_ex_web/live/machines/form_component.html.heex
+1 −1
@@ -21,7 +21,7 @@
21 21 field={{f, :type}}
22 22 type="select"
23 23 label="Machine Type"
24 − options={values}
24 + options={values()}
25 25 prompt="Select"
26 26 />
27 27 </div>
modified lib/lambent_ex_web/live/services/index.html.heex
+2 −0
@@ -60,6 +60,8 @@
60 60 <%= if v[:latency] do %>
61 61 <i class="fa-solid fa-table-tennis-paddle-ball"></i>&nbsp;<%= v[:latency] %> ms
62 62 <% end %>
63 + lights:
64 + <%= v[:d_lights] |> Map.keys |> length %>
63 65 <% else %>
64 66 down
65 67 <% end %>
modified mix.lock
+4 −4
@@ -4,8 +4,8 @@
4 4 "cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"},
5 5 "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
6 6 "cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"},
7 − "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"},
8 − "ecto": {:hex, :ecto, "3.9.2", "017db3bc786ff64271108522c01a5d3f6ba0aea5c84912cfb0dd73bf13684108", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "21466d5177e09e55289ac7eade579a642578242c7a3a9f91ad5c6583337a9d15"},
7 + "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
8 + "ecto": {:hex, :ecto, "3.10.3", "eb2ae2eecd210b4eb8bece1217b297ad4ff824b4384c0e3fdd28aaf96edd6135", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "44bec74e2364d491d70f7e42cd0d690922659d329f6465e89feb8a34e8cd3433"},
9 9 "esbuild": {:hex, :esbuild, "0.5.0", "d5bb08ff049d7880ee3609ed5c4b864bd2f46445ea40b16b4acead724fb4c4a3", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "f183a0b332d963c4cfaf585477695ea59eef9a6f2204fdd0efa00e099694ffe5"},
10 10 "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
11 11 "floki": {:hex, :floki, "0.34.0", "002d0cc194b48794d74711731db004fafeb328fe676976f160685262d43706a8", [:mix], [], "hexpm", "9c3a9f43f40dde00332a589bd9d389b90c1f518aef500364d00636acc5ebc99c"},
@@ -15,7 +15,7 @@
15 15 "heroicons": {:hex, :heroicons, "0.5.1", "cca0dcca07af5f74d8a7d111e40418d3615d65e6773c0ea10e20cef070fd30aa", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.2", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "4b096d0a1d50e9054df9b12cc637c9f65c3972ff086791d3f2d1846f0653117e"},
16 16 "httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"},
17 17 "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
18 − "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"},
18 + "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
19 19 "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
20 20 "mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"},
21 21 "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
@@ -39,7 +39,7 @@
39 39 "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
40 40 "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
41 41 "tailwind": {:hex, :tailwind, "0.1.9", "25ba09d42f7bfabe170eb67683a76d6ec2061952dc9bd263a52a99ba3d24bd4d", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "9213f87709c458aaec313bb5f2df2b4d2cedc2b630e4ae821bf3c54c47a56d0b"},
42 − "telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"},
42 + "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
43 43 "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
44 44 "telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"},
45 45 "tortoise": {:hex, :tortoise, "0.10.0", "d8821b9a5052355ca61d9fa784652f435997fa98b210f88565c98f3494605efd", [:mix], [{:gen_state_machine, "~> 2.0 or ~> 3.0", [hex: :gen_state_machine, repo: "hexpm", optional: false]}], "hexpm", "926d97b03cd0d2f6e0e1ebf06c1efa50101de96d0317dea5604c1c9e43e66735"},

Parents: 6792fd8