Browse Source

Implemented button polling, but no endpoints yet.

master
Gisle Aune 5 years ago
parent
commit
f892b83142
  1. 2
      cmd/lucifer-server/main.go
  2. 65
      database/sqlite/button-repository.go
  3. 12
      database/sqlite/init.go
  4. 2
      database/sqlite/light-repository.go
  5. 4
      light/driver.go
  6. 129
      light/hue/driver.go
  7. 244
      light/service.go
  8. 43
      models/button.go
  9. 240
      webui/package-lock.json

2
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)

65
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
}

12
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

2
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{}

4
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.

129
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 {

244
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),
}
}

43
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
}

240
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",

Loading…
Cancel
Save