neiam
/lambent_ex
Lambent Aether 5
public · Issues · Pulls 1 · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
Let a place pick up what is already sitting in it
65d680a · Gabriel Morell · 2026-09-17 01:12
Message
{commit_body(@commit)}
Files changed
modified
lib/lambent_ex/places.ex
+40
−1
@@ -40,7 +40,14 @@ defmodule LambentEx.Places do
| 40 | 40 | |
| 41 | 41 | Process.send_after(self(), :settled, settle()) |
| 42 | 42 | |
| 43 | − {:ok, %{defaults: M.get_place_defaults(), seen: %{}, links: %{}, settled: false}} | |
| 43 | + {:ok, | |
| 44 | + %{ | |
| 45 | + defaults: M.get_place_defaults(), | |
| 46 | + seen: %{}, | |
| 47 | + devices: %{}, | |
| 48 | + links: %{}, | |
| 49 | + settled: false | |
| 50 | + }} | |
| 44 | 51 | end |
| 45 | 52 | |
| 46 | 53 | defp settle, do: Application.get_env(:lambent_ex, :places_settle_ms, @settle) |
@@ -59,6 +66,17 @@ defmodule LambentEx.Places do
| 59 | 66 | GenServer.cast(__MODULE__, {:set_default, place, machine}) |
| 60 | 67 | end |
| 61 | 68 | |
| 69 | + @doc """ | |
| 70 | + Link everything in a place that nothing is driving yet. | |
| 71 | + | |
| 72 | + Adoption only happens as devices turn up or move, which is no help for the | |
| 73 | + ones that were already sitting there when the place got its default — the | |
| 74 | + first time you set one up, that is most of them. | |
| 75 | + """ | |
| 76 | + def adopt_place(place) do | |
| 77 | + GenServer.cast(__MODULE__, {:adopt_place, place}) | |
| 78 | + end | |
| 79 | + | |
| 62 | 80 | # impls |
| 63 | 81 | |
| 64 | 82 | @impl true |
@@ -78,6 +96,26 @@ defmodule LambentEx.Places do
| 78 | 96 | {:noreply, state |> put_defaults(defaults)} |
| 79 | 97 | end |
| 80 | 98 | |
| 99 | + @impl true | |
| 100 | + def handle_cast({:adopt_place, place}, state) do | |
| 101 | + case Map.get(state.defaults, place) do | |
| 102 | + nil -> | |
| 103 | + {:noreply, state} | |
| 104 | + | |
| 105 | + machine -> | |
| 106 | + state = | |
| 107 | + state.devices | |
| 108 | + |> Enum.filter(fn {mac, device} -> | |
| 109 | + device["place"] == place and device["type"] in @linkable and not driven?(mac, state) | |
| 110 | + end) | |
| 111 | + |> Enum.reduce(state, fn {mac, device}, state -> | |
| 112 | + link(mac, device, place, machine, state) | |
| 113 | + end) | |
| 114 | + | |
| 115 | + {:noreply, state} | |
| 116 | + end | |
| 117 | + end | |
| 118 | + | |
| 81 | 119 | defp put_defaults(state, defaults) do |
| 82 | 120 | M.put_place_defaults(defaults) |
| 83 | 121 | Phoenix.PubSub.broadcast(@pubsub_name, @pubsub_places, {:place_defaults, defaults}) |
@@ -94,6 +132,7 @@ defmodule LambentEx.Places do
| 94 | 132 | |
| 95 | 133 | @impl true |
| 96 | 134 | def handle_info({:devices_pub, devices}, state) do |
| 135 | + state = %{state | devices: Map.merge(state.devices, devices)} | |
| 97 | 136 | {:noreply, Enum.reduce(devices, state, &consider/2)} |
| 98 | 137 | end |
| 99 | 138 |
modified
lib/lambent_ex_web/live/devices/places.ex
+10
−0
@@ -63,6 +63,16 @@ defmodule LambentExWeb.DevicesLive.Places do
| 63 | 63 | {:noreply, socket} |
| 64 | 64 | end |
| 65 | 65 | |
| 66 | + # adoption happens as devices turn up, which is no help for the ones already | |
| 67 | + # sitting in a place when it gets its default | |
| 68 | + @impl true | |
| 69 | + def handle_event("adopt", %{"place" => place}, socket) do | |
| 70 | + waiting = waiting(socket.assigns.devices, socket.assigns.links, place) | |
| 71 | + Places.adopt_place(place) | |
| 72 | + | |
| 73 | + {:noreply, socket |> put_flash(:info, "Linking #{waiting} device(s) in #{place}...")} | |
| 74 | + end | |
| 75 | + | |
| 66 | 76 | defp blank_to_nil(""), do: nil |
| 67 | 77 | defp blank_to_nil(machine), do: machine |
| 68 | 78 |
modified
lib/lambent_ex_web/live/devices/places.html.heex
+13
−5
@@ -57,11 +57,19 @@
| 57 | 57 | </form> |
| 58 | 58 | </td> |
| 59 | 59 | <td> |
| 60 | − <%= if @defaults[place] do %> | |
| 61 | − <span class="text-sm opacity-60"> | |
| 62 | − <i class="fa-solid fa-thumbtack fa-fw"></i> pinned so it comes back | |
| 63 | − </span> | |
| 64 | − <% end %> | |
| 60 | + <div class="flex items-center gap-3"> | |
| 61 | + <%= if @defaults[place] && waiting(@devices, @links, place) > 0 do %> | |
| 62 | + <button class="btn btn-sm btn-primary" phx-click="adopt" phx-value-place={place}> | |
| 63 | + <i class="fa-solid fa-link"></i> | |
| 64 | + Link <%= waiting(@devices, @links, place) %> now | |
| 65 | + </button> | |
| 66 | + <% end %> | |
| 67 | + <%= if @defaults[place] do %> | |
| 68 | + <span class="text-sm opacity-60"> | |
| 69 | + <i class="fa-solid fa-thumbtack fa-fw"></i> pinned so it comes back | |
| 70 | + </span> | |
| 71 | + <% end %> | |
| 72 | + </div> | |
| 65 | 73 | </td> |
| 66 | 74 | </tr> |
| 67 | 75 | <% end %> |
modified
test/lambent_ex/places_test.exs
+69
−0
@@ -113,6 +113,75 @@ defmodule LambentEx.PlacesTest do
| 113 | 113 | drop_link("test-hall3-mover") |
| 114 | 114 | end |
| 115 | 115 | |
| 116 | + test "a place can pick up everything already sitting in it" do | |
| 117 | + {:ok, _pid} = | |
| 118 | + LambentEx.Machine.start_link(nil, | |
| 119 | + name: "places-machine-4", | |
| 120 | + step: LambentEx.Machine.Steps.Solid, | |
| 121 | + step_opts: %{h: 84}, | |
| 122 | + count: 1, | |
| 123 | + single: true | |
| 124 | + ) | |
| 125 | + | |
| 126 | + # two devices seen before the place had a default, so nothing adopted them | |
| 127 | + publish( | |
| 128 | + Map.merge( | |
| 129 | + device("aa:bb:cc:dd:ee:05", "test-hall5", "early-one"), | |
| 130 | + device("aa:bb:cc:dd:ee:06", "test-hall5", "early-two") | |
| 131 | + ) | |
| 132 | + ) | |
| 133 | + | |
| 134 | + refute "link-test-hall5-early-one" in link_names() | |
| 135 | + | |
| 136 | + Places.set_default("test-hall5", "places-machine-4") | |
| 137 | + Places.adopt_place("test-hall5") | |
| 138 | + Places.defaults() | |
| 139 | + | |
| 140 | + assert "link-test-hall5-early-one" in link_names() | |
| 141 | + assert "link-test-hall5-early-two" in link_names() | |
| 142 | + | |
| 143 | + drop_link("test-hall5-early-one") | |
| 144 | + drop_link("test-hall5-early-two") | |
| 145 | + end | |
| 146 | + | |
| 147 | + test "picking up a place twice does not double up" do | |
| 148 | + {:ok, _pid} = | |
| 149 | + LambentEx.Machine.start_link(nil, | |
| 150 | + name: "places-machine-5", | |
| 151 | + step: LambentEx.Machine.Steps.Solid, | |
| 152 | + step_opts: %{h: 84}, | |
| 153 | + count: 1, | |
| 154 | + single: true | |
| 155 | + ) | |
| 156 | + | |
| 157 | + publish(device("aa:bb:cc:dd:ee:07", "test-hall6", "only-one")) | |
| 158 | + Places.set_default("test-hall6", "places-machine-5") | |
| 159 | + Places.adopt_place("test-hall6") | |
| 160 | + Places.defaults() | |
| 161 | + | |
| 162 | + assert "link-test-hall6-only-one" in link_names() | |
| 163 | + | |
| 164 | + # the link publishes, so the second pass knows it is driven | |
| 165 | + Phoenix.PubSub.broadcast( | |
| 166 | + @pubsub, | |
| 167 | + "links_idx", | |
| 168 | + {:links_pub, | |
| 169 | + %{ | |
| 170 | + name: "test-hall6-only-one", | |
| 171 | + target: "aa:bb:cc:dd:ee:07", | |
| 172 | + source: "places-machine-5", | |
| 173 | + enabled: true | |
| 174 | + }} | |
| 175 | + ) | |
| 176 | + | |
| 177 | + Places.adopt_place("test-hall6") | |
| 178 | + Places.defaults() | |
| 179 | + | |
| 180 | + assert link_names() |> Enum.count(&(&1 == "link-test-hall6-only-one")) == 1 | |
| 181 | + | |
| 182 | + drop_link("test-hall6-only-one") | |
| 183 | + end | |
| 184 | + | |
| 116 | 185 | test "a default is remembered, and pins the machine it names" do |
| 117 | 186 | {:ok, pid} = |
| 118 | 187 | LambentEx.Machine.start_link(nil, |
modified
test/lambent_ex_web/live/places_test.exs
+23
−0
@@ -52,6 +52,29 @@ defmodule LambentExWeb.PlacesTest do
| 52 | 52 | assert render(live) =~ "pinned so it comes back" |
| 53 | 53 | end |
| 54 | 54 | |
| 55 | + test "the page offers to link what is sitting there unlinked", %{conn: conn} do | |
| 56 | + {:ok, live, _html} = live(conn, "/cfg/devices/places") | |
| 57 | + | |
| 58 | + Phoenix.PubSub.broadcast( | |
| 59 | + @pubsub, | |
| 60 | + "scan-82667777", | |
| 61 | + {:devices_pub, | |
| 62 | + %{"bb:bb:bb:bb:bb:02" => %{"place" => "page-hall", "type" => "7777-flex", "sox" => 2}}} | |
| 63 | + ) | |
| 64 | + | |
| 65 | + # nothing to offer until the place has a default | |
| 66 | + refute render(live) =~ "Link 1 now" | |
| 67 | + | |
| 68 | + Places.set_default("page-hall", "page-machine") | |
| 69 | + Places.defaults() | |
| 70 | + | |
| 71 | + html = render(live) | |
| 72 | + assert html =~ "Link 1 now" | |
| 73 | + | |
| 74 | + html = live |> element(~s(button[phx-click="adopt"])) |> render_click() | |
| 75 | + assert html =~ "Linking 1 device(s) in page-hall" | |
| 76 | + end | |
| 77 | + | |
| 55 | 78 | test "a default pointing at something not running says so", %{conn: conn} do |
| 56 | 79 | Places.set_default("page-hall", "long-gone") |
| 57 | 80 | Places.defaults() |
Parents: a1ddc30