From 0701a99eb0e124b48b5a609a8fafdf283ba28583 Mon Sep 17 00:00:00 2001 From: Cora Grant Date: Mon, 21 Oct 2024 16:15:40 -0400 Subject: [PATCH] chore: use static per-screen-type audio config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The central/motivating change here is a change to `screens-config-lib` that removes most audio-related parameters from the per-screen configs, since we'd like them to be the same for all screens of a given type. This creates a `Static` struct to encode/organize "static" parameters for each screen type — including candidate generators, refresh rate, periodic audio readout interval, and now various other audio settings. --- assets/src/components/admin/admin_tables.tsx | 24 +-- lib/screens/v2/screen_audio_data.ex | 90 +++------ lib/screens/v2/screen_data.ex | 6 +- lib/screens/v2/screen_data/layout.ex | 2 +- lib/screens/v2/screen_data/parameters.ex | 179 +++++++++++------ lib/screens/v2/screen_data/static.ex | 37 ++++ .../controllers/v2/screen_controller.ex | 8 +- mix.exs | 2 +- mix.lock | 2 +- test/screens/v2/screen_audio_data_test.exs | 184 +++--------------- .../v2/screen_data/parameters_test.exs | 145 ++++++++++++++ test/screens/v2/screen_data_test.exs | 18 +- 12 files changed, 375 insertions(+), 322 deletions(-) create mode 100644 lib/screens/v2/screen_data/static.ex create mode 100644 test/screens/v2/screen_data/parameters_test.exs diff --git a/assets/src/components/admin/admin_tables.tsx b/assets/src/components/admin/admin_tables.tsx index 4bd62ae08..0c0f01abf 100644 --- a/assets/src/components/admin/admin_tables.tsx +++ b/assets/src/components/admin/admin_tables.tsx @@ -242,15 +242,6 @@ const alertsColumn = { FormCell: FormTextarea, }; -const audioColumn = { - Header: "Audio", - accessor: buildAppParamAccessor("audio"), - mutator: buildAppParamMutator("audio"), - Cell: EditableTextarea, - disableFilters: true, - FormCell: FormTextarea, -}; - const DupV2ScreensTable = (): JSX.Element => { const columns = [ ...v2Columns, @@ -311,7 +302,6 @@ const GLEinkV2ScreensTable = (): JSX.Element => { departuresColumn, footerColumn, alertsColumn, - audioColumn, ]; const dataFilter = ({ app_id }) => { @@ -328,10 +318,21 @@ const BusShelterV2ScreensTable = (): JSX.Element => { const columns = [ ...v2Columns, + { + Header: "Audio Offset", + accessor: (row) => row.app_params.audio.interval_offset_seconds, + mutator: (row, value) => { + const newRow = structuredClone(row); + newRow.app_params.audio.interval_offset_seconds = value; + return newRow; + }, + Cell: EditableCell, + disableFilters: true, + FormCell: FormTextCell, + }, departuresColumn, footerColumn, alertsColumn, - audioColumn, { Header: "Survey", accessor: buildAppParamAccessor("survey"), @@ -408,7 +409,6 @@ const PreFareV2ScreensTable = (): JSX.Element => { disableFilters: true, FormCell: FormTextarea, }, - audioColumn, ]; return ; diff --git a/lib/screens/v2/screen_audio_data.ex b/lib/screens/v2/screen_audio_data.ex index b99f590a1..c207b3152 100644 --- a/lib/screens/v2/screen_audio_data.ex +++ b/lib/screens/v2/screen_audio_data.ex @@ -2,16 +2,13 @@ defmodule Screens.V2.ScreenAudioData do @moduledoc false alias Screens.Config.Cache - alias Screens.Util alias Screens.V2.ScreenData alias Screens.V2.ScreenData.Parameters alias Screens.V2.WidgetInstance - alias ScreensConfig.Screen - alias ScreensConfig.V2.{Audio, BusShelter, Busway, GlEink, PreFare} @type screen_id :: String.t() - @spec by_screen_id(screen_id()) :: list({module(), map()}) | :error + @spec by_screen_id(screen_id()) :: list({module(), map()}) def by_screen_id( screen_id, get_config_fn \\ &Cache.screen/1, @@ -20,32 +17,25 @@ defmodule Screens.V2.ScreenAudioData do now \\ DateTime.utc_now() ) do config = get_config_fn.(screen_id) - {:ok, now} = DateTime.shift_zone(now, "America/New_York") - case config do - %Screen{app_params: %app{}} when app not in [BusShelter, PreFare, GlEink, Busway] -> - :error - - %Screen{app_params: %_app{audio: audio}} -> - if date_in_range?(audio, now) do - visual_widgets_with_audio_equivalence = - config - |> generate_layout_fn.() - |> elem(1) - |> Map.values() - |> Enum.filter(&WidgetInstance.audio_valid_candidate?/1) - - audio_only_widgets = - visual_widgets_with_audio_equivalence - |> get_audio_only_instances_fn.(config) - |> Enum.filter(&WidgetInstance.audio_valid_candidate?/1) - - (visual_widgets_with_audio_equivalence ++ audio_only_widgets) - |> Enum.sort_by(&WidgetInstance.audio_sort_key/1) - |> Enum.map(&{WidgetInstance.audio_view(&1), WidgetInstance.audio_serialize(&1)}) - else - [] - end + if Parameters.audio_enabled?(config, now) do + visual_widgets_with_audio_equivalence = + config + |> generate_layout_fn.() + |> elem(1) + |> Map.values() + |> Enum.filter(&WidgetInstance.audio_valid_candidate?/1) + + audio_only_widgets = + visual_widgets_with_audio_equivalence + |> get_audio_only_instances_fn.(config) + |> Enum.filter(&WidgetInstance.audio_valid_candidate?/1) + + (visual_widgets_with_audio_equivalence ++ audio_only_widgets) + |> Enum.sort_by(&WidgetInstance.audio_sort_key/1) + |> Enum.map(&{WidgetInstance.audio_view(&1), WidgetInstance.audio_serialize(&1)}) + else + [] end end @@ -55,50 +45,16 @@ defmodule Screens.V2.ScreenAudioData do get_config_fn \\ &Cache.screen/1, now \\ DateTime.utc_now() ) do - config = get_config_fn.(screen_id) - {:ok, now} = DateTime.shift_zone(now, "America/New_York") - - case config do - %Screen{app_params: %app{}} when app not in [BusShelter] -> - :error - - %Screen{app_params: %_app{audio: audio}} -> - {:ok, get_volume(audio, now)} + case screen_id |> get_config_fn.() |> Parameters.audio_volume(now) do + nil -> :error + volume -> {:ok, volume} end end defp get_audio_only_instances(visual_widgets_with_audio_equivalence, config) do - candidate_generator = Parameters.get_candidate_generator(config) - - candidate_generator.audio_only_instances( + Parameters.candidate_generator(config).audio_only_instances( visual_widgets_with_audio_equivalence, config ) end - - defp get_volume( - %Audio{ - daytime_start_time: daytime_start_time, - daytime_stop_time: daytime_stop_time, - daytime_volume: daytime_volume, - nighttime_volume: nighttime_volume - }, - now - ) do - if Util.time_in_range?(now, daytime_start_time, daytime_stop_time), - do: daytime_volume, - else: nighttime_volume - end - - defp date_in_range?( - %Audio{ - start_time: start_time, - stop_time: stop_time, - days_active: days_active - }, - dt - ) do - Date.day_of_week(dt) in days_active and - Util.time_in_range?(DateTime.to_time(dt), start_time, stop_time) - end end diff --git a/lib/screens/v2/screen_data.ex b/lib/screens/v2/screen_data.ex index 68263b8c1..bb4cbd3bf 100644 --- a/lib/screens/v2/screen_data.ex +++ b/lib/screens/v2/screen_data.ex @@ -63,7 +63,7 @@ defmodule Screens.V2.ScreenData do selected_variant = Keyword.get(opts, :generator_variant) if Keyword.get(opts, :run_all_variants?, false) do - other_variants = List.delete([nil | @parameters.get_variants(config)], selected_variant) + other_variants = List.delete([nil | @parameters.variants(config)], selected_variant) Enum.each(other_variants, fn variant -> {:ok, _pid} = @@ -87,7 +87,7 @@ defmodule Screens.V2.ScreenData do ParallelRunSupervisor |> Task.Supervisor.async_stream( - [nil | @parameters.get_variants(config)], + [nil | @parameters.variants(config)], fn variant -> {variant, config |> Layout.generate(variant) |> then_fn.(config)} end @@ -114,7 +114,7 @@ defmodule Screens.V2.ScreenData do end defp resolve_paging(layout, config), - do: Layout.resolve_paging(layout, @parameters.get_refresh_rate(config)) + do: Layout.resolve_paging(layout, @parameters.refresh_rate(config)) @spec serialize(Layout.non_paged()) :: map() | nil def serialize({layout, instance_map, paging_metadata}) do diff --git a/lib/screens/v2/screen_data/layout.ex b/lib/screens/v2/screen_data/layout.ex index c09b0cc5e..9280b1de7 100644 --- a/lib/screens/v2/screen_data/layout.ex +++ b/lib/screens/v2/screen_data/layout.ex @@ -33,7 +33,7 @@ defmodule Screens.V2.ScreenData.Layout do @spec generate(Screen.t()) :: t() @spec generate(Screen.t(), String.t() | nil) :: t() def generate(config, variant \\ nil) do - candidate_generator = @parameters.get_candidate_generator(config, variant) + candidate_generator = @parameters.candidate_generator(config, variant) screen_template = candidate_generator.screen_template() config diff --git a/lib/screens/v2/screen_data/parameters.ex b/lib/screens/v2/screen_data/parameters.ex index 8e1e4044c..382cef612 100644 --- a/lib/screens/v2/screen_data/parameters.ex +++ b/lib/screens/v2/screen_data/parameters.ex @@ -1,88 +1,143 @@ defmodule Screens.V2.ScreenData.Parameters do @moduledoc false + alias Screens.Util alias Screens.V2.CandidateGenerator + alias Screens.V2.ScreenData.Static + alias Screens.V2.ScreenData.Static.PeriodicAudio + alias ScreensConfig.Screen - @app_id_to_candidate_generators %{ - bus_eink_v2: CandidateGenerator.BusEink, - bus_shelter_v2: CandidateGenerator.BusShelter, - gl_eink_v2: CandidateGenerator.GlEink, - busway_v2: CandidateGenerator.Busway, - solari_large_v2: CandidateGenerator.SolariLarge, - pre_fare_v2: CandidateGenerator.PreFare, - dup_v2: { - CandidateGenerator.Dup, - %{"new_departures" => CandidateGenerator.DupNew} + @all_times {~T[00:00:00], ~T[23:59:59]} + + @static_params %{ + bus_eink_v2: %Static{ + candidate_generator: CandidateGenerator.BusEink, + refresh_rate: 30 + }, + bus_shelter_v2: %Static{ + audio_active_time: {~T[04:45:00], ~T[01:45:00]}, + candidate_generator: CandidateGenerator.BusShelter, + periodic_audio: %PeriodicAudio{ + day_volume: 1.0, + interval_minutes: 5, + night_time: {~T[21:00:00], ~T[07:00:00]}, + night_volume: 0.5 + }, + refresh_rate: 20 + }, + busway_v2: %Static{ + audio_active_time: @all_times, + candidate_generator: CandidateGenerator.Busway, + refresh_rate: 15 + }, + dup_v2: %Static{ + candidate_generator: CandidateGenerator.Dup, + refresh_rate: 30, + variants: %{"new_departures" => CandidateGenerator.DupNew} + }, + elevator_v2: %Static{ + candidate_generator: CandidateGenerator.Elevator, + refresh_rate: 30 + }, + gl_eink_v2: %Static{ + audio_active_time: @all_times, + candidate_generator: CandidateGenerator.GlEink, + refresh_rate: 30 + }, + pre_fare_v2: %Static{ + audio_active_time: {~T[04:45:00], ~T[01:45:00]}, + candidate_generator: CandidateGenerator.PreFare, + refresh_rate: 20 }, - elevator_v2: CandidateGenerator.Elevator + solari_large_v2: %Static{ + candidate_generator: CandidateGenerator.SolariLarge, + refresh_rate: 15 + } } - @app_id_to_refresh_rate %{ - bus_eink_v2: 30, - bus_shelter_v2: 20, - gl_eink_v2: 30, - busway_v2: 15, - solari_large_v2: 15, - pre_fare_v2: 20, - dup_v2: 30, - elevator_v2: 30 - } + @typep static_params :: %{Screen.app_id() => Static.t()} - @app_id_to_audio_readout_interval %{ - bus_eink_v2: 0, - bus_shelter_v2: 5, - gl_eink_v2: 0, - busway_v2: 0, - solari_large_v2: 0, - pre_fare_v2: 0, - dup_v2: 0, - elevator_v2: 0 - } + @spec audio_enabled?(Screen.t(), DateTime.t()) :: boolean() + @spec audio_enabled?(Screen.t(), DateTime.t(), static_params()) :: boolean() + def audio_enabled?(%Screen{app_id: app_id}, now, static_params \\ @static_params) do + case Map.fetch!(static_params, app_id) do + %Static{audio_active_time: nil} -> + false - @callback get_candidate_generator(ScreensConfig.Screen.t()) :: module() - @callback get_candidate_generator(ScreensConfig.Screen.t(), String.t() | nil) :: module() - def get_candidate_generator(%ScreensConfig.Screen{app_id: app_id}, variant \\ nil) do - case Map.get(@app_id_to_candidate_generators, app_id) do - {default, _variants} when is_nil(variant) -> default - {_default, variants} -> Map.fetch!(variants, variant) - default -> default + %Static{audio_active_time: {start_time, end_time}} -> + Util.time_in_range?(now, start_time, end_time) end end - @callback get_variants(ScreensConfig.Screen.t()) :: [String.t()] - def get_variants(%ScreensConfig.Screen{app_id: app_id}) do - case Map.get(@app_id_to_candidate_generators, app_id) do - {_default, variants} -> Map.keys(variants) - _default -> [] + @spec audio_interval_minutes(Screen.app_id()) :: pos_integer() | nil + @spec audio_interval_minutes(Screen.app_id(), static_params()) :: pos_integer() | nil + def audio_interval_minutes(app_id, static_params \\ @static_params) do + case Map.fetch!(static_params, app_id) do + %Static{periodic_audio: nil} -> nil + %Static{periodic_audio: %PeriodicAudio{interval_minutes: interval}} -> interval end end - @callback get_refresh_rate(ScreensConfig.Screen.t() | atom()) :: pos_integer() | nil - def get_refresh_rate(%ScreensConfig.Screen{app_id: app_id}) do - get_refresh_rate(app_id) + @spec audio_interval_offset_seconds(Screen.t()) :: pos_integer() | nil + def audio_interval_offset_seconds(%Screen{ + app_params: %ScreensConfig.V2.BusShelter{ + audio: %ScreensConfig.V2.Audio{interval_offset_seconds: interval_offset_seconds} + } + }) do + interval_offset_seconds end - def get_refresh_rate(app_id) do - Map.get(@app_id_to_refresh_rate, app_id) - end + def audio_interval_offset_seconds(_screen), do: nil + + @spec audio_volume(Screen.t(), DateTime.t()) :: float() | nil + @spec audio_volume(Screen.t(), DateTime.t(), static_params()) :: float() | nil + def audio_volume(%Screen{app_id: app_id}, now, static_params \\ @static_params) do + case Map.fetch!(static_params, app_id) do + %Static{periodic_audio: nil} -> + nil - @spec get_audio_readout_interval(ScreensConfig.Screen.t() | atom()) :: pos_integer() | nil - def get_audio_readout_interval(%ScreensConfig.Screen{app_id: app_id}) do - get_refresh_rate(app_id) + %Static{ + periodic_audio: %PeriodicAudio{ + day_volume: day_volume, + night_time: {night_start, night_end}, + night_volume: night_volume + } + } -> + {:ok, now} = DateTime.shift_zone(now, "America/New_York") + if Util.time_in_range?(now, night_start, night_end), do: night_volume, else: day_volume + end end - def get_audio_readout_interval(app_id) do - Map.get(@app_id_to_audio_readout_interval, app_id) + @callback candidate_generator(Screen.t()) :: module() + @callback candidate_generator(Screen.t(), String.t() | nil) :: module() + @callback candidate_generator(Screen.t(), String.t() | nil, static_params()) :: module() + def candidate_generator( + %Screen{app_id: app_id}, + variant \\ nil, + static_params \\ @static_params + ) do + case Map.fetch!(static_params, app_id) do + %Static{candidate_generator: default} when is_nil(variant) -> default + %Static{variants: %{^variant => variant}} -> variant + end end - @spec get_audio_interval_offset_seconds(ScreensConfig.Screen.t()) :: pos_integer() - def get_audio_interval_offset_seconds(%ScreensConfig.Screen{ - app_params: %ScreensConfig.V2.BusShelter{ - audio: %ScreensConfig.V2.Audio{interval_offset_seconds: interval_offset_seconds} - } - }) do - interval_offset_seconds + @callback refresh_rate(Screen.t() | Screen.app_id()) :: pos_integer() | nil + @callback refresh_rate(Screen.t() | Screen.app_id(), static_params()) :: pos_integer() | nil + def refresh_rate(screen_or_app_id, static_params \\ @static_params) + + def refresh_rate(%Screen{app_id: app_id}, static_params), + do: refresh_rate(app_id, static_params) + + def refresh_rate(app_id, static_params) do + %Static{refresh_rate: refresh_rate} = Map.fetch!(static_params, app_id) + refresh_rate end - def get_audio_interval_offset_seconds(_screen), do: 0 + @callback variants(Screen.t()) :: [String.t()] + @callback variants(Screen.t(), static_params()) :: [String.t()] + def variants(%Screen{app_id: app_id}, static_params \\ @static_params) do + %Static{variants: variants} = Map.fetch!(static_params, app_id) + Map.keys(variants) + end end diff --git a/lib/screens/v2/screen_data/static.ex b/lib/screens/v2/screen_data/static.ex new file mode 100644 index 000000000..200efe3af --- /dev/null +++ b/lib/screens/v2/screen_data/static.ex @@ -0,0 +1,37 @@ +defmodule Screens.V2.ScreenData.Static do + @moduledoc "Encodes static configuration that is the same across all screens of a given type." + + @type time_range :: {start :: Time.t(), stop :: Time.t()} + + defmodule PeriodicAudio do + @moduledoc """ + Static configuration for screens that read out audio periodically, without rider action. + + Currently, screens that support periodic readouts happen to also be the only screens where we + can control the volume of readouts, so this configuration is bundled together. + """ + + alias Screens.V2.ScreenData.Static + + @type t :: %__MODULE__{ + day_volume: float(), + interval_minutes: pos_integer(), + night_time: Static.time_range(), + night_volume: float() + } + + @enforce_keys ~w[day_volume interval_minutes night_time night_volume]a + defstruct @enforce_keys + end + + @type t :: %__MODULE__{ + audio_active_time: time_range() | nil, + candidate_generator: module(), + periodic_audio: PeriodicAudio.t() | nil, + refresh_rate: pos_integer(), + variants: %{String.t() => module()} + } + + @enforce_keys ~w[candidate_generator refresh_rate]a + defstruct @enforce_keys ++ [audio_active_time: nil, periodic_audio: nil, variants: %{}] +end diff --git a/lib/screens_web/controllers/v2/screen_controller.ex b/lib/screens_web/controllers/v2/screen_controller.ex index 5b5fb3554..09d0958b9 100644 --- a/lib/screens_web/controllers/v2/screen_controller.ex +++ b/lib/screens_web/controllers/v2/screen_controller.ex @@ -58,7 +58,7 @@ defmodule ScreensWeb.V2.ScreenController do def index(conn, %{"id" => app_id}) when app_id in @app_id_strings do app_id = String.to_existing_atom(app_id) - refresh_rate = Parameters.get_refresh_rate(app_id) + refresh_rate = Parameters.refresh_rate(app_id) conn |> assign(:app_id, app_id) @@ -123,13 +123,13 @@ defmodule ScreensWeb.V2.ScreenController do end defp get_assigns(params, screen_id, %Screen{app_id: app_id} = config) do - refresh_rate = Parameters.get_refresh_rate(app_id) + refresh_rate = Parameters.refresh_rate(app_id) [ app_id: app_id, refresh_rate: refresh_rate, - audio_readout_interval: Parameters.get_audio_readout_interval(app_id), - audio_interval_offset_seconds: Parameters.get_audio_interval_offset_seconds(config), + audio_readout_interval: Parameters.audio_interval_minutes(app_id), + audio_interval_offset_seconds: Parameters.audio_interval_offset_seconds(config), sentry_dsn: if(params["disable_sentry"], do: nil, else: Sentry.get_dsn()), refresh_rate_offset: calculate_refresh_rate_offset(screen_id, refresh_rate), is_real_screen: match?(%{"is_real_screen" => "true"}, params), diff --git a/mix.exs b/mix.exs index f1cf68ea0..a41602cc5 100644 --- a/mix.exs +++ b/mix.exs @@ -87,7 +87,7 @@ defmodule Screens.MixProject do {:telemetry_metrics, "~> 0.4"}, {:screens_config, git: "https://github.com/mbta/screens-config-lib.git", - ref: "ff30a958e7d969e8c72c0391f621579427e88f03"}, + ref: "8ec6e1684a129b089edc5e867a32dfc90028b2e0"}, {:nebulex, "~> 2.6"}, {:remote_ip, "~> 1.2"}, {:hackney_telemetry, "~> 0.2.0"}, diff --git a/mix.lock b/mix.lock index ee66ebb01..3f75686bc 100644 --- a/mix.lock +++ b/mix.lock @@ -61,7 +61,7 @@ "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, "remote_ip": {:hex, :remote_ip, "1.2.0", "fb078e12a44414f4cef5a75963c33008fe169b806572ccd17257c208a7bc760f", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "2ff91de19c48149ce19ed230a81d377186e4412552a597d6a5137373e5877cb7"}, "retry": {:hex, :retry, "0.18.0", "dc58ebe22c95aa00bc2459f9e0c5400e6005541cf8539925af0aa027dc860543", [:mix], [], "hexpm", "9483959cc7bf69c9e576d9dfb2b678b71c045d3e6f39ab7c9aa1489df4492d73"}, - "screens_config": {:git, "https://github.com/mbta/screens-config-lib.git", "ff30a958e7d969e8c72c0391f621579427e88f03", [ref: "ff30a958e7d969e8c72c0391f621579427e88f03"]}, + "screens_config": {:git, "https://github.com/mbta/screens-config-lib.git", "8ec6e1684a129b089edc5e867a32dfc90028b2e0", [ref: "8ec6e1684a129b089edc5e867a32dfc90028b2e0"]}, "sentry": {:hex, :sentry, "10.7.1", "33392222d80ccff99c503f972998d2858b4c1e5aca2219a34269b68dacba8e7d", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_ownership, "~> 0.3.0 or ~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 0.20", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.6", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "56291312397bf2b6afab6cf4f7aa1f27413b0eb2ceeb63b8aab2d7658aaea882"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "stream_data": {:hex, :stream_data, "1.1.2", "05499eaec0443349ff877aaabc6e194e82bda6799b9ce6aaa1aadac15a9fdb4d", [:mix], [], "hexpm", "129558d2c77cbc1eb2f4747acbbea79e181a5da51108457000020a906813a1a9"}, diff --git a/test/screens/v2/screen_audio_data_test.exs b/test/screens/v2/screen_audio_data_test.exs index 14c71741a..8f312f9e0 100644 --- a/test/screens/v2/screen_audio_data_test.exs +++ b/test/screens/v2/screen_audio_data_test.exs @@ -7,8 +7,9 @@ defmodule Screens.V2.ScreenAudioDataTest do setup do %{ - config_no_audio: %Screen{ + config_bus_shelter: %Screen{ app_params: %V2.BusShelter{ + audio: %V2.Audio{interval_enabled: true}, departures: %V2.Departures{sections: []}, footer: %V2.Footer{}, header: %V2.Header.CurrentStopId{stop_id: "1"}, @@ -19,66 +20,24 @@ defmodule Screens.V2.ScreenAudioDataTest do name: "TEST", app_id: :bus_shelter_v2 }, - config_audio_inactive: %Screen{ - app_params: %V2.BusShelter{ - audio: %V2.Audio{ - start_time: ~T[00:00:00], - stop_time: ~T[01:00:00], - daytime_start_time: ~T[00:00:00], - daytime_stop_time: ~T[01:00:00], - days_active: [1, 2, 3, 4, 5, 6, 7], - daytime_volume: 0.1, - nighttime_volume: 0.1 - }, - departures: %V2.Departures{sections: []}, - footer: %V2.Footer{}, + config_dup: %Screen{ + app_params: %V2.Dup{ + primary_departures: %V2.Departures{sections: []}, + secondary_departures: %V2.Departures{sections: []}, header: %V2.Header.CurrentStopId{stop_id: "1"}, alerts: %V2.Alerts{stop_id: "1"} }, - vendor: :lg_mri, + vendor: :outfront, device_id: "TEST", name: "TEST", - app_id: :bus_shelter_v2 - }, - config_valid_audio: %Screen{ - app_params: %V2.BusShelter{ - audio: %V2.Audio{ - start_time: ~T[00:00:00], - stop_time: ~T[02:00:00], - daytime_start_time: ~T[00:00:00], - daytime_stop_time: ~T[01:00:00], - days_active: [1, 2, 3, 4, 5, 6, 7], - daytime_volume: 0.1, - nighttime_volume: 0.1 - }, - departures: %V2.Departures{sections: []}, - footer: %V2.Footer{}, - header: %V2.Header.CurrentStopId{stop_id: "1"}, - alerts: %V2.Alerts{stop_id: "1"} - }, - vendor: :lg_mri, - device_id: "TEST", - name: "TEST", - app_id: :bus_shelter_v2 - }, - config_eink: %Screen{ - app_params: %V2.BusEink{ - departures: %V2.Departures{sections: []}, - footer: %V2.Footer{}, - header: %V2.Header.CurrentStopId{stop_id: "1"}, - alerts: %V2.Alerts{stop_id: "1"} - }, - vendor: :gds, - device_id: "TEST", - name: "TEST", - app_id: :bus_eink_v2 + app_id: :dup_v2 } } end describe "by_screen_id/3" do test "returns a list of {audio_view, view_assigns_map} tuples", %{ - config_valid_audio: config_valid_audio + config_bus_shelter: config_bus_shelter } do screen_id = "123" now = ~U[2021-10-18T05:00:00Z] @@ -104,28 +63,24 @@ defmodule Screens.V2.ScreenAudioDataTest do } } - get_config_fn = fn _screen_id -> config_valid_audio end - - fetch_data_fn = fn _config_valid_audio -> {:layout, selected_instances} end - + get_config_fn = fn _screen_id -> config_bus_shelter end + generate_layout_fn = fn _config_valid_audio -> {:layout, selected_instances} end get_audio_only_instances_fn = fn _widgets, _config -> [] end - audio_view = ScreensWeb.V2.Audio.MockWidgetView - expected_data = [{audio_view, %{content: "Header"}}, {audio_view, %{content: "Departures"}}] assert expected_data == ScreenAudioData.by_screen_id( screen_id, get_config_fn, - fetch_data_fn, + generate_layout_fn, get_audio_only_instances_fn, now ) end - test "returns an empty list if audio not in valid date range", %{ - config_audio_inactive: config_audio_inactive + test "returns empty list if screen type does not have audio enabled", %{ + config_dup: config_dup } do screen_id = "123" now = ~U[2021-10-18T15:00:00Z] @@ -151,116 +106,22 @@ defmodule Screens.V2.ScreenAudioDataTest do } } - get_config_fn = fn _screen_id -> config_audio_inactive end - - fetch_data_fn = fn _config_audio_inactive -> {:layout, selected_instances} end - + get_config_fn = fn _screen_id -> config_dup end + generate_layout_fn = fn _config_eink -> {:layout, selected_instances} end get_audio_only_instances_fn = fn _widgets, _config -> [] end - expected_data = [] - - assert expected_data == + assert [] == ScreenAudioData.by_screen_id( screen_id, get_config_fn, - fetch_data_fn, - get_audio_only_instances_fn, - now - ) - end - - test "returns an empty list if audio config is missing", %{ - config_no_audio: config_no_audio - } do - screen_id = "123" - now = ~U[2021-10-18T15:00:00Z] - - selected_instances = %{ - {0, :medium_left} => %MockWidget{ - slot_names: [:medium_left, :medium_right], - audio_valid_candidate?: false, - audio_sort_key: [2], - content: "Alert" - }, - :main_content => %MockWidget{ - slot_names: [:main_content], - audio_valid_candidate?: true, - audio_sort_key: [1], - content: "Departures" - }, - :header => %MockWidget{ - slot_names: [:header], - audio_valid_candidate?: true, - audio_sort_key: [0], - content: "Header" - } - } - - get_config_fn = fn _screen_id -> config_no_audio end - - fetch_data_fn = fn _config_no_audio -> {:layout, selected_instances} end - - get_audio_only_instances_fn = fn _widgets, _config -> [] end - - expected_data = [] - - assert expected_data == - ScreenAudioData.by_screen_id( - screen_id, - get_config_fn, - fetch_data_fn, - get_audio_only_instances_fn, - now - ) - end - - test "returns an error if not a screen with defined audio equivalence", %{ - config_eink: config_eink - } do - screen_id = "123" - now = ~U[2021-10-18T15:00:00Z] - - selected_instances = %{ - {0, :medium_left} => %MockWidget{ - slot_names: [:medium_left, :medium_right], - audio_valid_candidate?: false, - audio_sort_key: [2], - content: "Alert" - }, - :main_content => %MockWidget{ - slot_names: [:main_content], - audio_valid_candidate?: true, - audio_sort_key: [1], - content: "Departures" - }, - :header => %MockWidget{ - slot_names: [:header], - audio_valid_candidate?: true, - audio_sort_key: [0], - content: "Header" - } - } - - get_config_fn = fn _screen_id -> config_eink end - - fetch_data_fn = fn _config_eink -> {:layout, selected_instances} end - - get_audio_only_instances_fn = fn _widgets, _config -> [] end - - expected_data = :error - - assert expected_data == - ScreenAudioData.by_screen_id( - screen_id, - get_config_fn, - fetch_data_fn, + generate_layout_fn, get_audio_only_instances_fn, now ) end test "adds audio-only widgets to the readout data as defined by the audio_only_instances candidate generator function", - %{config_valid_audio: config_valid_audio} do + %{config_bus_shelter: config_bus_shelter} do screen_id = "123" now = ~U[2021-10-18T05:00:00Z] @@ -285,9 +146,8 @@ defmodule Screens.V2.ScreenAudioDataTest do } } - get_config_fn = fn _screen_id -> config_valid_audio end - - fetch_data_fn = fn _config_valid_audio -> {:layout, selected_instances} end + get_config_fn = fn _screen_id -> config_bus_shelter end + generate_layout_fn = fn _config_valid_audio -> {:layout, selected_instances} end get_audio_only_instances_fn = fn _widgets, _config -> [ @@ -326,7 +186,7 @@ defmodule Screens.V2.ScreenAudioDataTest do ScreenAudioData.by_screen_id( screen_id, get_config_fn, - fetch_data_fn, + generate_layout_fn, get_audio_only_instances_fn, now ) diff --git a/test/screens/v2/screen_data/parameters_test.exs b/test/screens/v2/screen_data/parameters_test.exs new file mode 100644 index 000000000..cff029250 --- /dev/null +++ b/test/screens/v2/screen_data/parameters_test.exs @@ -0,0 +1,145 @@ +defmodule Screens.V2.ScreenData.ParametersTest do + use ExUnit.Case, async: true + + alias ScreensConfig.{Screen, V2} + alias Screens.V2.ScreenData.{Parameters, Static} + alias Screens.V2.ScreenData.Static.PeriodicAudio + + @app_id :bus_shelter_v2 + + defp build_params(static_fields) do + %{bus_shelter_v2: struct!(%Static{candidate_generator: nil, refresh_rate: 0}, static_fields)} + end + + defp build_periodic_audio(fields) do + struct!( + %PeriodicAudio{ + day_volume: 1.0, + interval_minutes: 7, + night_time: {~T[00:00:00], ~T[00:00:00]}, + night_volume: 0.5 + }, + fields + ) + end + + defp build_screen(app_params \\ %{}) do + %Screen{ + app_id: :bus_shelter_v2, + app_params: + struct!( + %V2.BusShelter{ + alerts: %V2.Alerts{stop_id: "1"}, + departures: %V2.Departures{sections: []}, + footer: %V2.Footer{}, + header: %V2.Header.CurrentStopId{stop_id: "1"} + }, + app_params + ), + device_id: "TEST", + name: "TEST", + vendor: :lg_mri + } + end + + defp local_time(time), do: DateTime.new!(~D[2024-01-01], time, "America/New_York") + + describe "audio_enabled?/2" do + test "is false for a screen type with no audio schedule" do + static_params = build_params(audio_active_time: nil) + refute Parameters.audio_enabled?(build_screen(), DateTime.utc_now(), static_params) + end + + test "is false outside the screen type's audio schedule" do + static_params = build_params(audio_active_time: {~T[06:00:00], ~T[08:00:00]}) + refute Parameters.audio_enabled?(build_screen(), local_time(~T[09:00:00]), static_params) + end + + test "is true within the screen type's audio schedule" do + static_params = build_params(audio_active_time: {~T[06:00:00], ~T[08:00:00]}) + assert Parameters.audio_enabled?(build_screen(), local_time(~T[07:00:00]), static_params) + end + end + + describe "audio_interval_minutes/1" do + test "is nil for a screen type without periodic audio" do + static_params = build_params(periodic_audio: nil) + assert Parameters.audio_interval_minutes(@app_id, static_params) == nil + end + + test "is the configured interval for a screen type with periodic audio" do + static_params = build_params(periodic_audio: build_periodic_audio(interval_minutes: 7)) + assert Parameters.audio_interval_minutes(@app_id, static_params) == 7 + end + end + + describe "audio_interval_offset_seconds/1" do + test "is nil for a screen without periodic audio" do + screen = build_screen(audio: nil) + assert Parameters.audio_interval_offset_seconds(screen) == nil + end + + test "is the configured offset for a screen with periodic audio" do + screen = build_screen(audio: %V2.Audio{interval_offset_seconds: 90}) + assert Parameters.audio_interval_offset_seconds(screen) == 90 + end + end + + describe "audio_volume/2" do + setup do + %{ + static_params: + build_params( + periodic_audio: + build_periodic_audio( + day_volume: 1.0, + night_time: {~T[22:00:00], ~T[04:00:00]}, + night_volume: 0.5 + ) + ) + } + end + + test "is nil for a screen type without periodic audio" do + now = local_time(~T[00:00:00]) + static_params = build_params(periodic_audio: nil) + assert Parameters.audio_volume(build_screen(), now, static_params) == nil + end + + test "is the day volume when it is not nighttime", %{static_params: static_params} do + now = local_time(~T[10:00:00]) + assert Parameters.audio_volume(build_screen(), now, static_params) == 1.0 + end + + test "is the night volume when it is nighttime", %{static_params: static_params} do + now = local_time(~T[01:00:00]) + assert Parameters.audio_volume(build_screen(), now, static_params) == 0.5 + end + end + + describe "candidate_generator/2" do + test "returns the candidate generator for a screen type" do + static_params = build_params(candidate_generator: :default) + assert Parameters.candidate_generator(build_screen(), nil, static_params) == :default + end + + test "returns a variant candidate generator" do + static_params = build_params(candidate_generator: :default, variants: %{"test" => :variant}) + assert Parameters.candidate_generator(build_screen(), "test", static_params) == :variant + end + end + + describe "refresh_rate/1" do + test "returns the configured refresh rate for a screen type" do + static_params = build_params(refresh_rate: 25) + assert Parameters.refresh_rate(build_screen(), static_params) == 25 + end + end + + describe "variants/1" do + test "returns the list of variants for a screen type" do + static_params = build_params(variants: %{"one" => :test1, "two" => :test2}) + assert Parameters.variants(build_screen(), static_params) == ["one", "two"] + end + end +end diff --git a/test/screens/v2/screen_data_test.exs b/test/screens/v2/screen_data_test.exs index 374d6066e..c7f358d5b 100644 --- a/test/screens/v2/screen_data_test.exs +++ b/test/screens/v2/screen_data_test.exs @@ -52,7 +52,7 @@ defmodule Screens.V2.ScreenDataTest do describe "get/2" do setup do - stub(MockParameters, :get_refresh_rate, fn _app_id -> 0 end) + stub(MockParameters, :refresh_rate, fn _app_id -> 0 end) :ok end @@ -68,7 +68,7 @@ defmodule Screens.V2.ScreenDataTest do expect( MockParameters, - :get_candidate_generator, + :candidate_generator, fn %Screen{app_id: :test_app}, nil -> GrayGenerator end ) @@ -81,7 +81,7 @@ defmodule Screens.V2.ScreenDataTest do expect( MockParameters, - :get_candidate_generator, + :candidate_generator, fn %Screen{app_id: :test_app}, nil -> GrayGenerator end ) @@ -94,7 +94,7 @@ defmodule Screens.V2.ScreenDataTest do expect( MockParameters, - :get_candidate_generator, + :candidate_generator, fn %Screen{app_id: :test_app}, "test_variant" -> GrayGenerator end ) @@ -107,11 +107,11 @@ defmodule Screens.V2.ScreenDataTest do build_config(%{app_id: :test_app, app_params: %{test_pid: self()}}) end) - expect(MockParameters, :get_variants, fn %Screen{app_id: :test_app} -> ["crash"] end) + expect(MockParameters, :variants, fn %Screen{app_id: :test_app} -> ["crash"] end) stub( MockParameters, - :get_candidate_generator, + :candidate_generator, fn %Screen{app_id: :test_app}, nil -> GrayGenerator %Screen{app_id: :test_app}, "crash" -> CrashGenerator @@ -132,17 +132,17 @@ defmodule Screens.V2.ScreenDataTest do describe "variants/2" do setup do - stub(MockParameters, :get_refresh_rate, fn _app_id -> 0 end) + stub(MockParameters, :refresh_rate, fn _app_id -> 0 end) :ok end test "gets widget data for all variants" do expect(MockCache, :screen, fn "test_id" -> build_config(%{app_id: :test_app}) end) - expect(MockParameters, :get_variants, fn %Screen{app_id: :test_app} -> ["green"] end) + expect(MockParameters, :variants, fn %Screen{app_id: :test_app} -> ["green"] end) stub( MockParameters, - :get_candidate_generator, + :candidate_generator, fn %Screen{app_id: :test_app}, nil -> GrayGenerator %Screen{app_id: :test_app}, "green" -> GreenGenerator