From f892b83142c7682c35c787f91f4565d5f1a1d605 Mon Sep 17 00:00:00 2001 From: Gisle Aune Date: Sun, 26 May 2019 17:13:44 +0200 Subject: [PATCH] Implemented button polling, but no endpoints yet. --- cmd/lucifer-server/main.go | 2 +- database/sqlite/button-repository.go | 65 +++++++ database/sqlite/init.go | 12 ++ database/sqlite/light-repository.go | 2 +- light/driver.go | 4 + light/hue/driver.go | 129 +++++++++++++- light/service.go | 244 +++++++++++++++++++++++++-- models/button.go | 43 +++++ webui/package-lock.json | 240 +------------------------- 9 files changed, 490 insertions(+), 251 deletions(-) create mode 100644 database/sqlite/button-repository.go create mode 100644 models/button.go diff --git a/cmd/lucifer-server/main.go b/cmd/lucifer-server/main.go index 89cde78..a356e0c 100644 --- a/cmd/lucifer-server/main.go +++ b/cmd/lucifer-server/main.go @@ -50,7 +50,7 @@ func main() { setupAdmin(sqlite.UserRepository, sqlite.GroupRepository) // Services - lightService := light.NewService(sqlite.BridgeRepository, sqlite.LightRepository) + lightService := light.NewService(sqlite.BridgeRepository, sqlite.LightRepository, sqlite.GroupRepository, sqlite.ButtonRepository) // Controllers userController := controllers.NewUserController(sqlite.UserRepository, sqlite.SessionRepository) diff --git a/database/sqlite/button-repository.go b/database/sqlite/button-repository.go new file mode 100644 index 0000000..6d204f1 --- /dev/null +++ b/database/sqlite/button-repository.go @@ -0,0 +1,65 @@ +package sqlite + +import ( + "context" + + "git.aiterp.net/lucifer/lucifer/models" +) + +type buttonRepository struct{} + +// ButtonRepository is a sqlite datbase repository for the Button model. +var ButtonRepository models.ButtonRepository = &buttonRepository{} + +func (r *buttonRepository) FindByID(ctx context.Context, id int) (models.Button, error) { + button := models.Button{} + err := db.GetContext(ctx, &button, "SELECT * FROM button WHERE id=?", id) + + return button, err +} + +func (r *buttonRepository) FindByInternalID(ctx context.Context, internalID string) (models.Button, error) { + button := models.Button{} + err := db.GetContext(ctx, &button, "SELECT * FROM button WHERE internal_id=?", internalID) + + return button, err +} + +func (r *buttonRepository) List(ctx context.Context) ([]models.Button, error) { + lights := make([]models.Button, 0, 64) + err := db.SelectContext(ctx, &lights, "SELECT * FROM button") + + return lights, err +} + +func (r *buttonRepository) ListByBridge(ctx context.Context, bridge models.Bridge) ([]models.Button, error) { + lights := make([]models.Button, 0, 64) + err := db.SelectContext(ctx, &lights, "SELECT * FROM button WHERE bridge_id=?", bridge.ID) + + return lights, err +} + +func (r *buttonRepository) Insert(ctx context.Context, button models.Button) (models.Button, error) { + res, err := db.NamedExecContext(ctx, "INSERT INTO button (bridge_id, internal_index, internal_id, target_group_id, name, kind, missing, num_buttons) VALUES(:bridge_id, :internal_index, :internal_id, :target_group_id, :name, :kind, :missing, :num_buttons)", button) + if err != nil { + return models.Button{}, err + } + + id, err := res.LastInsertId() + if err != nil { + return models.Button{}, err + } + + button.ID = int(id) + return button, nil +} + +func (r *buttonRepository) Update(ctx context.Context, button models.Button) error { + _, err := db.NamedExecContext(ctx, "UPDATE button SET internal_index=:internal_index,missing=:missing WHERE id=:id", button) + return err +} + +func (r *buttonRepository) Remove(ctx context.Context, button models.Button) error { + _, err := db.NamedExecContext(ctx, "DELETE FROM button WHERE id=:id", button) + return err +} diff --git a/database/sqlite/init.go b/database/sqlite/init.go index 1949ae7..e36eda3 100644 --- a/database/sqlite/init.go +++ b/database/sqlite/init.go @@ -85,6 +85,18 @@ CREATE TABLE IF NOT EXISTS "light" ( brightness INTEGER NOT NULL ); +CREATE TABLE IF NOT EXISTS "button" ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + bridge_id INTEGER NOT NULL, + internal_index INTEGER NOT NULL, + internal_id VARCHAR(255) NOT NULL, + target_group_id INTEGER NOT NULL, + name VARCHAR(255) NOT NULL, + kind VARCHAR(255) NOT NULL, + missing BOOLEAN NOT NULL, + num_buttons INTEGER NOT NULL +); + CREATE TABLE IF NOT EXISTS "group" ( id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(255) NOT NULL diff --git a/database/sqlite/light-repository.go b/database/sqlite/light-repository.go index 5a44bb8..72c522f 100644 --- a/database/sqlite/light-repository.go +++ b/database/sqlite/light-repository.go @@ -10,7 +10,7 @@ import ( type lightRepository struct{} // LightRepository is a sqlite datbase repository for the Light model. -var LightRepository = &lightRepository{} +var LightRepository models.LightRepository = &lightRepository{} func (r *lightRepository) FindByID(ctx context.Context, id int) (models.Light, error) { light := models.Light{} diff --git a/light/driver.go b/light/driver.go index f5a1d8f..a59f20d 100644 --- a/light/driver.go +++ b/light/driver.go @@ -30,6 +30,10 @@ type Driver interface { // ChangedLights returns a subset of the list describing which lights to update. ChangedLights(ctx context.Context, bridge models.Bridge, lights ...models.Light) ([]models.Light, error) + + Buttons(ctx context.Context, bridge models.Bridge) ([]models.Button, error) + + PollButton(ctx context.Context, bridge models.Bridge, button models.Button) (<-chan models.ButtonEvent, error) } // RegisterDriver registers a driver. This must happen in init() functions. diff --git a/light/hue/driver.go b/light/hue/driver.go index 3515985..8beb3e9 100644 --- a/light/hue/driver.go +++ b/light/hue/driver.go @@ -61,8 +61,7 @@ func (d *driver) Apply(ctx context.Context, bridge models.Bridge, lights ...mode continue } - // Prevent race condition since `hueLight` changes per iteration. - hl := hueLight + hl := hueLight // `hueLight` will change while the gorouting below still needs it. eg.Go(func() error { if !light.On { @@ -115,6 +114,132 @@ func (d *driver) DiscoverLights(ctx context.Context, bridge models.Bridge) error return hueBridge.FindNewLights() } +func (d *driver) PollButton(ctx context.Context, bridge models.Bridge, button models.Button) (<-chan models.ButtonEvent, error) { + hueBridge, err := d.getBridge(bridge) + if err != nil { + return nil, err + } + + channel := make(chan models.ButtonEvent, 60) + + go func() { + fastTicker := time.NewTicker(time.Second / 30) + slowTicker := time.NewTicker(time.Second / 3) + ticker := slowTicker + + checkTicker := time.NewTicker(time.Second * 5) + + gotPress := make([]bool, button.NumButtons+1) + lastEventTime := time.Now() + lastEvent := uint16(0) + lastButton := 0 + + for { + select { + case <-ticker.C: + { + sensor, err := hueBridge.GetSensorByIndex(button.InternalIndex) + if err != nil { + log.Println("Sensor poll error:", err) + continue + } + + if sensor.State.LastUpdated.Time == nil || sensor.State.LastUpdated.Before(lastEventTime) { + continue + } + if sensor.State.LastUpdated.Equal(lastEventTime) && lastEvent == sensor.State.ButtonEvent { + continue + } + + if ticker != fastTicker { + ticker = fastTicker + } + + buttonIndex := int(sensor.State.ButtonEvent) / 1000 + buttonEvent := int(sensor.State.ButtonEvent) % 1000 + + // Slip in a press event if there's a release not associated with a press + if buttonEvent >= 2 { + if !gotPress[buttonIndex] { + channel <- models.ButtonEvent{Index: buttonIndex, Kind: models.ButtonEventKindPress} + } + } + + // Slip in a release event if the last button was pressed but the release got lost betwen polls + if lastButton != 0 && buttonIndex != lastButton && gotPress[lastButton] { + channel <- models.ButtonEvent{Index: lastButton, Kind: models.ButtonEventKindRelease} + } + + lastEvent = sensor.State.ButtonEvent + lastEventTime = *sensor.State.LastUpdated.Time + lastButton = buttonIndex + + switch buttonEvent { + case 0: + // Slip in a release event if this was a consecutive press but the release got lost betwen polls + if lastButton == buttonIndex && gotPress[lastButton] { + channel <- models.ButtonEvent{Index: lastButton, Kind: models.ButtonEventKindRelease} + } + + gotPress[buttonIndex] = true + channel <- models.ButtonEvent{Index: buttonIndex, Kind: models.ButtonEventKindPress} + case 1: + gotPress[buttonIndex] = true + channel <- models.ButtonEvent{Index: buttonIndex, Kind: models.ButtonEventKindRepeat} + case 2, 3: + gotPress[buttonIndex] = false + channel <- models.ButtonEvent{Index: buttonIndex, Kind: models.ButtonEventKindRelease} + } + } + case <-checkTicker.C: + { + if ticker != slowTicker && time.Since(lastEventTime) > time.Second*3 { + ticker = slowTicker + } + } + case <-ctx.Done(): + { + ticker.Stop() + close(channel) + return + } + } + } + }() + + return channel, nil +} + +func (d *driver) Buttons(ctx context.Context, bridge models.Bridge) ([]models.Button, error) { + hueBridge, err := d.getBridge(bridge) + if err != nil { + return nil, err + } + + sensors, err := hueBridge.GetAllSensors() + if err != nil { + return nil, err + } + + buttons := make([]models.Button, 0, len(sensors)) + for _, sensor := range sensors { + if sensor.Type == "ZLLSwitch" { + buttons = append(buttons, models.Button{ + ID: -1, + BridgeID: bridge.ID, + InternalIndex: sensor.Index, + InternalID: sensor.UniqueID, + Name: sensor.Name, + Kind: sensor.Type, + NumButtons: 4, + TargetGroupID: -1, + }) + } + } + + return buttons, nil +} + func (d *driver) Lights(ctx context.Context, bridge models.Bridge) ([]models.Light, error) { hueBridge, err := d.getBridge(bridge) if err != nil { diff --git a/light/service.go b/light/service.go index 10a4f82..c164c20 100644 --- a/light/service.go +++ b/light/service.go @@ -2,6 +2,7 @@ package light import ( "context" + "database/sql" "log" "net/http" "sync" @@ -9,6 +10,7 @@ import ( "git.aiterp.net/lucifer/lucifer/internal/httperr" "git.aiterp.net/lucifer/lucifer/models" + "golang.org/x/sync/errgroup" ) // ErrUnknownDriver is returned by any function asking for a driver name if the driver specified doesn't exist. @@ -20,6 +22,11 @@ type Service struct { bridges models.BridgeRepository lights models.LightRepository + buttons models.ButtonRepository + groups models.GroupRepository + + buttonActive map[int]bool + buttonTargets map[int]int } // DirectConnect connects to a bridge directly, without going through the discovery process to find them.. @@ -51,9 +58,6 @@ func (s *Service) DirectConnect(ctx context.Context, driver string, addr string, // SyncLights syncs all lights in a bridge with the state in the database. func (s *Service) SyncLights(ctx context.Context, bridge models.Bridge) error { - s.mutex.Lock() - defer s.mutex.Unlock() - d, ok := drivers[bridge.Driver] if !ok { return ErrUnknownDriver @@ -107,16 +111,111 @@ func (s *Service) SyncLights(ctx context.Context, bridge models.Bridge) error { return nil } -// UpdateLight updates the light immediately. -func (s *Service) UpdateLight(ctx context.Context, light models.Light) error { - s.mutex.Lock() - defer s.mutex.Unlock() +// SyncButtons syncs all buttons in a bridge with the state in the database. +func (s *Service) SyncButtons(ctx context.Context, bridge models.Bridge) error { + d, ok := drivers[bridge.Driver] + if !ok { + return ErrUnknownDriver + } - err := s.lights.Update(ctx, light) + bridgeButtons, err := d.Buttons(ctx, bridge) if err != nil { return err } + dbButtons, err := s.buttons.ListByBridge(ctx, bridge) + if err != nil { + return err + } + + changedButtons := make([]models.Button, 0, len(bridgeButtons)) + newButtons := make([]models.Button, 0, len(dbButtons)) + exists := make(map[string]bool, len(dbButtons)) + + // Check for new or changed + for _, bridgeButton := range bridgeButtons { + found := false + + exists[bridgeButton.InternalID] = true + + for _, dbButton := range dbButtons { + if bridgeButton.InternalID == dbButton.InternalID { + if dbButton.InternalIndex != bridgeButton.InternalIndex || dbButton.Missing { + dbButton.InternalIndex = bridgeButton.InternalIndex + dbButton.Missing = false + changedButtons = append(changedButtons, dbButton) + + log.Println("Updating button", dbButton.ID) + } + + found = true + + break + } + } + + if !found { + newButtons = append(newButtons, bridgeButton) + } + } + + // Check for missing buttons + for _, dbButton := range dbButtons { + if !exists[dbButton.InternalID] { + dbButton.Missing = true + changedButtons = append(changedButtons, dbButton) + + log.Println("Marking button", dbButton.ID, "as missing") + } + } + + // Insert new buttons + for _, newButton := range newButtons { + button, err := s.buttons.Insert(ctx, newButton) + if err != nil { + log.Printf("Failed to insert button %s (%s): %s", button.Name, button.InternalID, err) + continue + } + + dbButtons = append(dbButtons, button) + } + + // Update changed buttons + for _, changedButton := range changedButtons { + err := s.buttons.Update(ctx, changedButton) + if err != nil { + log.Printf("Failed to change button %s (%d): %s", changedButton.Name, changedButton.ID, err) + continue + } + + for i := range dbButtons { + if dbButtons[i].ID == changedButton.ID { + dbButtons[i] = changedButton + + break + } + } + } + + // Start polling buttons + for _, dbButton := range dbButtons { + s.mutex.Lock() + if exists[dbButton.InternalID] && !s.buttonActive[dbButton.ID] { + s.buttonActive[dbButton.ID] = true + go s.pollButton(ctx, bridge, dbButton) + + log.Printf("Polling button %s (%d)", dbButton.Name, dbButton.ID) + } + + s.buttonTargets[dbButton.ID] = dbButton.TargetGroupID + s.mutex.Unlock() + } + + return nil +} + +// UpdateLight updates the light immediately. +func (s *Service) UpdateLight(ctx context.Context, light models.Light) error { bridge, err := s.bridges.FindByID(ctx, light.BridgeID) if err != nil { return httperr.NotFound("Bridge") @@ -126,6 +225,11 @@ func (s *Service) UpdateLight(ctx context.Context, light models.Light) error { return ErrUnknownDriver } + err = s.lights.Update(ctx, light) + if err != nil { + return err + } + return d.Apply(ctx, bridge, light) } @@ -146,7 +250,14 @@ func (s *Service) SyncLoop(ctx context.Context) { for _, bridge := range bridges { err = s.SyncLights(ctx, bridge) if err != nil { - log.Println("Sync failed:", err) + log.Printf("Light sync failed for bridge %s (%d): %s", bridge.Name, bridge.ID, err) + break + } + + err = s.SyncButtons(ctx, bridge) + if err != nil { + log.Printf("Button sync failed for bridge %s (%d): %s", bridge.Name, bridge.ID, err) + break } } } @@ -274,10 +385,123 @@ func (s *Service) DiscoverBridges(ctx context.Context, driver string) ([]models. return newBridges, nil } +func (s *Service) pollButton(ctx context.Context, bridge models.Bridge, button models.Button) { + defer func() { + s.mutex.Lock() + s.buttonActive[button.ID] = false + s.mutex.Unlock() + }() + + d, ok := drivers[bridge.Driver] + if !ok { + log.Printf("Could not listen on button %s (%d) because the driver %s is unknwon", button.Name, button.ID, bridge.Driver) + return + } + + events, err := d.PollButton(ctx, bridge, button) + if err != nil { + log.Printf("Could not listen on button %s (%d) because the driver %s is unknwon", button.Name, button.ID, bridge.Driver) + return + } + + for event := range events { + s.mutex.Lock() + targetGroupID := s.buttonTargets[button.ID] + s.mutex.Unlock() + + if targetGroupID < 0 { + continue + } + + if event.Kind != models.ButtonEventKindPress && event.Kind != models.ButtonEventKindRepeat { + continue + } + + group, err := s.groups.FindByID(ctx, targetGroupID) + if err != nil { + if err != sql.ErrNoRows { + log.Println("Group not found:", err, targetGroupID) + } + + continue + } + + lights, err := s.lights.ListByGroup(ctx, group) + + log.Printf("Got button input for group %s (%d) (id: %d, idx: %d, kind: %s)", group.Name, group.ID, button.ID, event.Index, event.Kind) + + eg, _ := errgroup.WithContext(ctx) + + for i := range lights { + light := &lights[i] + + switch event.Index { + case 1: + { + if !light.On { + light.On = true + + eg.Go(func() error { + return s.UpdateLight(ctx, *light) + }) + } + } + case 2: + { + if light.Brightness < 254 { + if light.Brightness >= (254 - 64) { + light.Brightness = 254 + } else { + light.Brightness += 64 + } + + eg.Go(func() error { + return s.UpdateLight(ctx, *light) + }) + } + } + case 3: + { + if light.Brightness > 0 { + if light.Brightness < 64 { + light.Brightness = 0 + } else { + light.Brightness -= 64 + } + + eg.Go(func() error { + return s.UpdateLight(ctx, *light) + }) + } + } + case 4: + { + if light.On { + light.On = false + eg.Go(func() error { + return s.UpdateLight(ctx, *light) + }) + } + } + } + } + + err = eg.Wait() + if err != nil { + log.Println("Failed to update one or more lights:", err) + } + } +} + // NewService creates a new light.Service. -func NewService(bridges models.BridgeRepository, lights models.LightRepository) *Service { +func NewService(bridges models.BridgeRepository, lights models.LightRepository, groups models.GroupRepository, buttons models.ButtonRepository) *Service { return &Service{ bridges: bridges, lights: lights, + buttons: buttons, + groups: groups, + + buttonActive: make(map[int]bool, 64), + buttonTargets: make(map[int]int, 64), } } diff --git a/models/button.go b/models/button.go new file mode 100644 index 0000000..a2cc03b --- /dev/null +++ b/models/button.go @@ -0,0 +1,43 @@ +package models + +import "context" + +// A Button is general information about a button that exists. +type Button struct { + ID int `db:"id" json:"id"` + BridgeID int `db:"bridge_id" json:"bridgeId"` + InternalIndex int `db:"internal_index" json:"internalIndex"` + InternalID string `db:"internal_id" json:"internalId"` + TargetGroupID int `db:"target_group_id" json:"targetGroupId"` + Name string `db:"name" json:"name"` + Kind string `db:"kind" json:"kind"` + Missing bool `db:"missing" json:"missing"` + NumButtons int `db:"num_buttons" json:"numButtons"` +} + +// ButtonEvent is an event. +type ButtonEvent struct { + Index int + Kind string +} + +const ( + // ButtonEventKindPress is a button event. + ButtonEventKindPress = "press" + // ButtonEventKindRepeat is a button event. + ButtonEventKindRepeat = "repeat" + // ButtonEventKindRelease is a button event. + ButtonEventKindRelease = "release" +) + +// ButtonRepository is an interface for all database operations +// related to the button model. +type ButtonRepository interface { + FindByID(ctx context.Context, id int) (Button, error) + FindByInternalID(ctx context.Context, internalID string) (Button, error) + List(ctx context.Context) ([]Button, error) + ListByBridge(ctx context.Context, bridge Bridge) ([]Button, error) + Insert(ctx context.Context, button Button) (Button, error) + Update(ctx context.Context, button Button) error + Remove(ctx context.Context, button Button) error +} diff --git a/webui/package-lock.json b/webui/package-lock.json index ed19833..bb8c6eb 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -844,11 +844,6 @@ "resolved": "https://registry.npmjs.org/@csstools/convert-colors/-/convert-colors-1.4.0.tgz", "integrity": "sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==" }, - "@icons/material": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz", - "integrity": "sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==" - }, "@jaames/iro": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@jaames/iro/-/iro-4.0.1.tgz", @@ -1159,14 +1154,6 @@ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.1.1.tgz", "integrity": "sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw==" }, - "add-dom-event-listener": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/add-dom-event-listener/-/add-dom-event-listener-1.1.0.tgz", - "integrity": "sha512-WCxx1ixHT0GQU9hb0KI/mhgRQhnU+U3GvwY6ZvVjYq8rsihIGoaIOUbY0yMPBxLH5MDtr0kz3fisWGNcbWW7Jw==", - "requires": { - "object-assign": "4.x" - } - }, "address": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/address/-/address-1.0.3.tgz", @@ -2999,24 +2986,11 @@ "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" }, - "component-classes": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/component-classes/-/component-classes-1.2.6.tgz", - "integrity": "sha1-xkI5TDYYpNiwuJGe/Mu9kw5c1pE=", - "requires": { - "component-indexof": "0.0.3" - } - }, "component-emitter": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" }, - "component-indexof": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/component-indexof/-/component-indexof-0.0.3.tgz", - "integrity": "sha1-EdCRMSI5648yyPJa6csAL/6NPCQ=" - }, "compressible": { "version": "2.0.15", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.15.tgz", @@ -3199,16 +3173,6 @@ "sha.js": "^2.4.8" } }, - "create-react-class": { - "version": "15.6.3", - "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.6.3.tgz", - "integrity": "sha512-M+/3Q6E6DLO6Yx3OwrWjwHBnvfXXYA7W+dFjt/ZDBemHO1DDZhsalX/NUtnTYclN6GfnBDRh4qRHjcDHmlJBJg==", - "requires": { - "fbjs": "^0.8.9", - "loose-envify": "^1.3.1", - "object-assign": "^4.1.1" - } - }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -3239,15 +3203,6 @@ "randomfill": "^1.0.3" } }, - "css-animation": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/css-animation/-/css-animation-1.5.0.tgz", - "integrity": "sha512-hWYoWiOZ7Vr20etzLh3kpWgtC454tW5vn4I6rLANDgpzNSkO7UfOqyCEeaoBSG9CYWQpRkFWTWbWW8o3uZrNLw==", - "requires": { - "babel-runtime": "6.x", - "component-classes": "^1.2.5" - } - }, "css-color-names": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", @@ -3866,11 +3821,6 @@ "esutils": "^2.0.2" } }, - "dom-align": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/dom-align/-/dom-align-1.8.0.tgz", - "integrity": "sha512-B85D4ef2Gj5lw0rK0KM2+D5/pH7yqNxg2mB+E8uzFaolpm7RQmsxEfjyEuNiF8UBBkffumYDeKRzTzc3LePP+w==" - }, "dom-converter": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", @@ -4020,14 +3970,6 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, - "encoding": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", - "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", - "requires": { - "iconv-lite": "~0.4.13" - } - }, "end-of-stream": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", @@ -5033,35 +4975,6 @@ "bser": "^2.0.0" } }, - "fbjs": { - "version": "0.8.17", - "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz", - "integrity": "sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=", - "requires": { - "core-js": "^1.0.0", - "isomorphic-fetch": "^2.1.1", - "loose-envify": "^1.0.0", - "object-assign": "^4.1.0", - "promise": "^7.1.1", - "setimmediate": "^1.0.5", - "ua-parser-js": "^0.7.18" - }, - "dependencies": { - "core-js": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", - "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=" - }, - "promise": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", - "requires": { - "asap": "~2.0.3" - } - } - } - }, "figgy-pudding": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz", @@ -6872,9 +6785,9 @@ "dev": true }, "is-glob": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", - "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", "dev": true, "requires": { "is-extglob": "^2.1.1" @@ -7432,15 +7345,6 @@ "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" }, - "isomorphic-fetch": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", - "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", - "requires": { - "node-fetch": "^1.0.1", - "whatwg-fetch": ">=0.10.0" - } - }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -8406,11 +8310,6 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" }, - "lodash._getnative": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", - "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=" - }, "lodash._reinterpolate": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", @@ -8426,16 +8325,6 @@ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" }, - "lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=" - }, - "lodash.isarray": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", - "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=" - }, "lodash.isfunction": { "version": "3.0.9", "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", @@ -8446,16 +8335,6 @@ "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-3.0.2.tgz", "integrity": "sha1-PI+41bW/S/kK4G4U8qUwpO2TXh0=" }, - "lodash.keys": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", - "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", - "requires": { - "lodash._getnative": "^3.0.0", - "lodash.isarguments": "^3.0.0", - "lodash.isarray": "^3.0.0" - } - }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -8574,11 +8453,6 @@ "object-visit": "^1.0.0" } }, - "material-colors": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", - "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==" - }, "math-random": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz", @@ -8917,15 +8791,6 @@ "lower-case": "^1.1.1" } }, - "node-fetch": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", - "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", - "requires": { - "encoding": "^0.1.11", - "is-stream": "^1.0.1" - } - }, "node-forge": { "version": "0.7.5", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.5.tgz", @@ -11546,66 +11411,6 @@ } } }, - "rc-align": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/rc-align/-/rc-align-2.4.5.tgz", - "integrity": "sha512-nv9wYUYdfyfK+qskThf4BQUSIadeI/dCsfaMZfNEoxm9HwOIioQ+LyqmMK6jWHAZQgOzMLaqawhuBXlF63vgjw==", - "requires": { - "babel-runtime": "^6.26.0", - "dom-align": "^1.7.0", - "prop-types": "^15.5.8", - "rc-util": "^4.0.4" - } - }, - "rc-animate": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/rc-animate/-/rc-animate-2.6.0.tgz", - "integrity": "sha512-JXDycchgbOI+7T/VKmFWnAIn042LLScK1fNkmNunb0jz5q5aPGCAybx2bTo7X5t31Jkj9OsxKNb/vZPDPWufCg==", - "requires": { - "babel-runtime": "6.x", - "classnames": "^2.2.6", - "css-animation": "^1.3.2", - "prop-types": "15.x", - "raf": "^3.4.0", - "react-lifecycles-compat": "^3.0.4" - } - }, - "rc-color-picker": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/rc-color-picker/-/rc-color-picker-1.2.6.tgz", - "integrity": "sha512-AaC9Pg7qCHSy5M4eVbqDIaNb2FC4SEw82GOHB2C4R/+vF2FVa/r5XA+Igg5+zLPmAvBLhz9tL4MAfkRA8yWNJw==", - "requires": { - "classnames": "^2.2.5", - "prop-types": "^15.5.8", - "rc-trigger": "1.x", - "rc-util": "^4.0.2", - "tinycolor2": "^1.4.1" - } - }, - "rc-trigger": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/rc-trigger/-/rc-trigger-1.11.5.tgz", - "integrity": "sha512-MBuUPw1nFzA4K7jQOwb7uvFaZFjXGd00EofUYiZ+l/fgKVq8wnLC0lkv36kwqM7vfKyftRo2sh7cWVpdPuNnnw==", - "requires": { - "babel-runtime": "6.x", - "create-react-class": "15.x", - "prop-types": "15.x", - "rc-align": "2.x", - "rc-animate": "2.x", - "rc-util": "4.x" - } - }, - "rc-util": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-4.6.0.tgz", - "integrity": "sha512-rbgrzm1/i8mgfwOI4t1CwWK7wGe+OwX+dNa7PVMgxZYPBADGh86eD4OcJO1UKGeajIMDUUKMluaZxvgraQIOmw==", - "requires": { - "add-dom-event-listener": "^1.1.0", - "babel-runtime": "6.x", - "prop-types": "^15.5.10", - "shallowequal": "^0.2.2" - } - }, "react": { "version": "16.8.1", "resolved": "https://registry.npmjs.org/react/-/react-16.8.1.tgz", @@ -11647,19 +11452,6 @@ } } }, - "react-color": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.17.0.tgz", - "integrity": "sha512-kJfE5tSaFe6GzalXOHksVjqwCPAsTl+nzS9/BWfP7j3EXbQ4IiLAF9sZGNzk3uq7HfofGYgjmcUgh0JP7xAQ0w==", - "requires": { - "@icons/material": "^0.2.4", - "lodash": ">4.17.4", - "material-colors": "^1.2.1", - "prop-types": "^15.5.10", - "reactcss": "^1.2.0", - "tinycolor2": "^1.4.1" - } - }, "react-dev-utils": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-7.0.1.tgz", @@ -11932,14 +11724,6 @@ "react-lifecycles-compat": "^3.0.4" } }, - "reactcss": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", - "integrity": "sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==", - "requires": { - "lodash": "^4.0.1" - } - }, "reactn": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reactn/-/reactn-0.2.2.tgz", @@ -13237,14 +13021,6 @@ } } }, - "shallowequal": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-0.2.2.tgz", - "integrity": "sha1-HjL9W8q2rWiKSBLLDMBO/HXHAU4=", - "requires": { - "lodash.keys": "^3.1.2" - } - }, "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", @@ -14048,11 +13824,6 @@ "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=" }, - "tinycolor2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz", - "integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g=" - }, "tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -14194,11 +13965,6 @@ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, - "ua-parser-js": { - "version": "0.7.19", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.19.tgz", - "integrity": "sha512-T3PVJ6uz8i0HzPxOF9SWzWAlfN/DavlpQqepn22xgve/5QecC+XMCAtmUNnY7C9StehaV6exjUCI801lOI7QlQ==" - }, "uglify-js": { "version": "3.4.9", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz",