An open API service indexing awesome lists of open source software.

https://github.com/nicklayb/snowhite

Smart mirror application written in Elixir for better concurrency and availability
https://github.com/nicklayb/snowhite

concurrent mirror smart-mirror

Last synced: 12 months ago
JSON representation

Smart mirror application written in Elixir for better concurrency and availability

Awesome Lists containing this project

README

          

# Snowhite

> Mirror mirror, tell me who is the most beautiful

![Snowhite demo](demo.gif "Snowhite demo")

[Blog article about Snowhite](https://nboisvert.com/blog/so-i-built-a-smart-mirror-using-elixir)

## Fetching deps

You need to fetch both node and elixir deps before playing with it. You can use the `make deps` target to do so.

## Initial setup

### Creating the project
Create a new phoenix project

```bash
mix phx.new my_mirror --no-ecto
```

*If you want to use Ecto, you can, but Snowhite doesn't require it.*

### Creating the profiles

Snowhite makes use of different profile to better split information. You can create multiple profile with multiple modules for different use cases. You first need to create the profile manager like the following. You **must** at least include a `:default` profile.

We recommend that you pass in your timezone so the scheduler runs within your timezone. It fallbacks to UTC but if you want to use, for instance, the module Suntime, it might run at weird time instead of 1h am if you do not set your timezone. Unless, of course, if you live in the middle of the planet.

```elixir
defmodule MyMirror.Profiles do
use Snowhite, timezone: "America/Toronto"

profile(:default, MyMirror.Profiles.Default)
end
```

You can create as much profile as you want as long as their name differs. You can switch between profile using either a param `?profile=another` or a `X-Snowhite-Profile: another` header. It would then load the `:another` profile instead of `:default`

#### Creating a profile and registering modules

You can register any module in the Profile module using the `register_module/3` macro.

```elixir
defmodule MyMirror.Profiles.Default do
use Snowhite.Builder.Profile

configure(
locale: "fr"
)

register_module(:top_left, Snowhite.Modules.Clock) # will be french
register_module(:top_left, Snowhite.Modules.Clock, locale: "en") # will be english
end
```

It expects:
- A position; any of `[top|middle|bottom]_[left|center|right]` (ex. `top_left`, `bottom_right`, etc...)
- A module using either a `Snowhite.Builder.Module` or `Phoenix.LiveView`; Some examples modules are available in `lib/modules` [see here for custom modules](#creating-modules).
- A keyword list of options specific to module.

### Route the profiles

In your Phoenix's router, add the following call

```elixir
defmodule SnowhiteNboisvertWeb.Router do
#...
import Snowhite, only: [snowhite_router: 1]

pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
end

pipe_through :browser # Make sure you piped through the browser pipeline

snowhite_router(SnowhiteNboisvert.Profiles)
end
```

Snowhite renders on `/`, so if you want to scope it under `/mirror`, for instance, you can do the following

```elixir
scope "/mirror" do
snowhite_router(SnowhiteNboisvert.Profiles)
end
```

### Assets

Snowhite requires at least a js file located at `../deps/snowhite/assets/js/live.js`. Even though it does **not** require a CSS file, you might want to import the one provided with Snowhite to have basic styling at `../deps/snowhite/assets/css/app.scss`.

## Creating modules

You can create your own modules using either the `Snowhite.Builder.Module` or a any raw `Phoenix.LiveView` component.

For most use case, you might prefer to use the `Snowhite.Builder.Module` as it includes [some convenient functions](#convenient-functions). To use Phoenix.LiveView, refer to [the documentation](https://hexdocs.pm/phoenix_live_view).

The only required function is `render/1` as shown below.

```elixir
defmodule Snowhite.Modules.HelloWorld do
use Snowhite.Builder.Module

def render(assigns) do
~L"""

Hello world.


"""
end
end
```

### Other assigns

If you need other assigns in your module, you must override `mount/1` to define those. That function recieves a socket with `options` and `params` assigned.

```elixir
defmodule Snowhite.Modules.HelloWorld do
use Snowhite.Builder.Module

def mount(socket) do
assign(socket, :message, "Hello, you weird person")
end

def render(assigns) do
~L"""

@message


"""
end
end
```

### Using options

You must first defined supported options like the following

```elixir
defmodule Snowhite.Modules.HelloWorld do
use Snowhite.Builder.Module

def module_options do
%{
message: :required, # Raises if :message option is missing
color: {:optional, "white"} # Sets "white" if :color is missing
}
end

# ...
end
```

You can then pass in options like below

```elixir
defmodule MyMirror.Profiles.Default do
use Snowhite.Builder.Profile

register_module(:top_left, Snowhite.Modules.HelloWorld, message: "Hello, punk.")
end
```

And access those options in the socket assigns under the `options` key.

```elixir
defmodule Snowhite.Modules.HelloWorld do
use Snowhite.Builder.Module

def module_options do
%{
message: :required,
color: {:optional, "white"}
}
end

def render(%{options: %{color: color, message: message}} = assigns) do
~L"""

<%= message %>


"""
end
end
```

#### Raises for bad options

If you were to provide an unsupported option to a module, it would raise. This is an expected behaviour as it could help spotting typos or unintended option passing.

```elixir
defmodule Snowhite.Modules.HelloWorld do
use Snowhite.Builder.Module

def module_options do
%{
message: :required, # Raises if :message option is missing
color: {:optional, "white"} # Sets "white" if :color is missing
}
end

# ...
end

defmodule MyMirror.Profiles.Default do
use Snowhite.Builder.Profile

register_module(:top_left, Snowhite.Modules.HelloWorld, message: "Hello, punk.", locale: "fr")
end
```

In this example, an exception would raise saying that `{:locale, "fr"}` is not supported as option.

### Data source

Even though it can work perfectly, we recommend creating genserver to keep/update the data your module will use. Mainly because opening 3 instances of the same Snowhite app will use one central source of truth but also because they will be kept in perfect sync. If you keep the data in the live view, opening three instances of the same module will load three times the same data and will be out of sync unless you are able to start the three of the at the e x a c t same time.

Default modules in Snowhite all includes a server, take a look to have a better understanding of it.

### Convenient functions

#### Periodically sending events

The best way of working with periodic events is to add a Server that implements a GenServer to your module. Doing so will ensure that all instances of the app are sharing the exact same data and prevent visual failure as they will occur in the server. (See existing modules as inspiration)

Howerver, some modules might require some refresh/update at some point. To do so, you have access to the following helpers:

- `every(ms, name, func)`: Will run a function every `ms` milliseconds under the event `name`. **Note**: Every `name` must be unique as it refers to `handle_info/2` event name. If you want your module to have a configurable scheduled event, you can pass an atom instead of an integer for the `ms`. It will fetch the given option key from the assign instead of using an hardcoded timing

**Bonus**: To write miliseconds in a readable way, there is a sigil `~d` that helps you write clocks. It supports hours, minutes and seconds in the following format
```elixir
~d(1h) # 3_600_000
~d(1m) # 60_000
~d(1s) # 1_000
~d(6h30m1s) # 23_401_000
```

### CSS

You might need some css to make this beautiful. To do so, create a file under `assets/css/modules/my_module.scss` and register it in `assets/css/modules/_modules.scss`. Now all you need is to fulfill the file.

The module is scoped under a div that has the module name as class. If your module is named `Snowhite.Modules.SomeNice.Module`, the class will be `snowhite-modules-somenice-module`. It is recommended that you scope all your styling under this. If you want to override anything else (colors, layout etc...), edit it inside over `_override.scss`.

## Start dev server

We use `direnv` to setup environment. So create or edit `~/.envrc` to add env variable if you need to prefixed with `SNOWHITE_`.

Example to override `PORT`, you add `export SNOWHITE_PORT=1234` in `~/.envrc` and you server will be exposed under `1234`. (**Note**: Any change require server restart)

Start the dev server using `make dev`

## TODO

- [] Write tests
- [] Extract OpenWeather client to it's own package
- [] Improve documentations
- [] Write guides
- [] Support multiple application names so we can have two clock with different city, for instance.

[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/D1D2YX9OU)