Explorar el Código

Add support for transitioning from on/off

Christopher Mullins hace 6 años
padre
commit
a7d9d49cae

+ 67 - 23
lib/MiLight/MiLightClient.cpp

@@ -300,7 +300,8 @@ void MiLightClient::update(JsonObject request) {
     this->updateBeginHandler();
   }
 
-  const uint8_t parsedStatus = this->parseStatus(request);
+  const JsonVariant status = this->extractStatus(request);
+  const uint8_t parsedStatus = this->parseStatus(status);
   const JsonVariant jsonTransition = request[RequestKeys::TRANSITION];
   float transition = 0;
 
@@ -316,7 +317,11 @@ void MiLightClient::update(JsonObject request) {
 
   // Always turn on first
   if (parsedStatus == ON) {
-    this->updateStatus(ON);
+    if (transition == 0) {
+      this->updateStatus(ON);
+    } else {
+      handleTransition(GroupStateField::STATUS, status, transition);
+    }
   }
 
   for (const char* fieldName : FIELD_ORDERINGS) {
@@ -345,7 +350,11 @@ void MiLightClient::update(JsonObject request) {
 
   // Always turn off last
   if (parsedStatus == OFF) {
-    this->updateStatus(OFF);
+    if (transition == 0) {
+      this->updateStatus(OFF);
+    } else {
+      handleTransition(GroupStateField::STATUS, status, transition);
+    }
   }
 
   if (this->updateEndHandler) {
@@ -429,6 +438,19 @@ void MiLightClient::handleTransition(GroupStateField field, JsonVariant value, f
       currentColor,
       endColor
     );
+  } else if (field == GroupStateField::STATUS || field == GroupStateField::STATE) {
+    uint8_t startLevel = 0;
+    MiLightStatus status = parseMilightStatus(value);
+
+    if (currentState->isSetBrightness()) {
+      startLevel = currentState->getBrightness();
+    } else if (status == ON) {
+      startLevel = 0;
+    } else {
+      startLevel = 100;
+    }
+
+    transitionBuilder = transitions.buildStatusTransition(bulbId, parseMilightStatus(value), startLevel);
   } else {
     uint16_t currentValue = currentState->getParsedFieldValue(field);
     uint16_t endValue = value;
@@ -452,13 +474,16 @@ void MiLightClient::handleTransition(GroupStateField field, JsonVariant value, f
 
 bool MiLightClient::handleTransition(JsonObject args, JsonDocument& responseObj) {
   if (! args.containsKey(FS(TransitionParams::FIELD))
-    || ! args.containsKey(FS(TransitionParams::START_VALUE))
     || ! args.containsKey(FS(TransitionParams::END_VALUE))) {
     responseObj[F("error")] = F("Ignoring transition missing required arguments");
     return false;
   }
 
+  const BulbId& bulbId = currentRemote->packetFormatter->currentBulbId();
   const char* fieldName = args[FS(TransitionParams::FIELD)];
+  const GroupState* groupState = stateStore->get(bulbId);
+  JsonVariant startValue = args[FS(TransitionParams::START_VALUE)];
+  JsonVariant endValue = args[FS(TransitionParams::END_VALUE)];
   GroupStateField field = GroupStateFieldHelpers::getFieldByName(fieldName);
   std::shared_ptr<Transition::Builder> transitionBuilder = nullptr;
 
@@ -477,11 +502,14 @@ bool MiLightClient::handleTransition(JsonObject args, JsonDocument& responseObj)
     case GroupStateField::LEVEL:
     case GroupStateField::KELVIN:
     case GroupStateField::COLOR_TEMP:
+
       transitionBuilder = transitions.buildFieldTransition(
-        currentRemote->packetFormatter->currentBulbId(),
+        bulbId,
         field,
-        args[FS(TransitionParams::START_VALUE)],
-        args[FS(TransitionParams::END_VALUE)]
+        startValue.isUndefined()
+          ? groupState->getParsedFieldValue(field)
+          : startValue.as<uint16_t>(),
+        endValue
       );
       break;
 
@@ -491,10 +519,12 @@ bool MiLightClient::handleTransition(JsonObject args, JsonDocument& responseObj)
 
   // Color can be decomposed into hue/saturation and these can be transitioned separately
   if (field == GroupStateField::COLOR) {
-    ParsedColor startColor = ParsedColor::fromJson(args[FS(TransitionParams::START_VALUE)]);
-    ParsedColor endColor = ParsedColor::fromJson(args[FS(TransitionParams::END_VALUE)]);
+    ParsedColor _startValue = startValue.isUndefined()
+      ? groupState->getColor()
+      : ParsedColor::fromJson(startValue);
+    ParsedColor endColor = ParsedColor::fromJson(endValue);
 
-    if (! startColor.success) {
+    if (! _startValue.success) {
       responseObj[F("error")] = F("Transition - error parsing start color");
       return false;
     }
@@ -504,12 +534,27 @@ bool MiLightClient::handleTransition(JsonObject args, JsonDocument& responseObj)
     }
 
     transitionBuilder = transitions.buildColorTransition(
-      currentRemote->packetFormatter->currentBulbId(),
-      startColor,
+      bulbId,
+      _startValue,
       endColor
     );
   }
 
+  // Status is handled a little differently
+  if (field == GroupStateField::STATUS || field == GroupStateField::STATE) {
+    MiLightStatus toStatus = parseMilightStatus(endValue);
+    uint8_t startLevel;
+    if (groupState->isSetBrightness()) {
+      startLevel = groupState->getBrightness();
+    } else if (toStatus == ON) {
+      startLevel = 0;
+    } else {
+      startLevel = 100;
+    }
+
+    transitionBuilder = transitions.buildStatusTransition(bulbId, toStatus, startLevel);
+  }
+
   if (transitionBuilder == nullptr) {
     char errorMsg[30];
     sprintf_P(errorMsg, PSTR("Recognized, but unsupported transition field: %s\n"), fieldName);
@@ -541,23 +586,22 @@ void MiLightClient::handleEffect(const String& effect) {
   }
 }
 
-uint8_t MiLightClient::parseStatus(JsonObject object) {
+JsonVariant MiLightClient::extractStatus(JsonObject object) {
   JsonVariant status;
 
-  if (object.containsKey(GroupStateFieldNames::STATUS)) {
-    status = object[GroupStateFieldNames::STATUS];
-  } else if (object.containsKey(GroupStateFieldNames::STATE)) {
-    status = object[GroupStateFieldNames::STATE];
+  if (object.containsKey(FS(GroupStateFieldNames::STATUS))) {
+    return object[FS(GroupStateFieldNames::STATUS)];
   } else {
-    return 255;
+    return object[FS(GroupStateFieldNames::STATE)];
   }
+}
 
-  if (status.is<bool>()) {
-    return status.as<bool>() ? ON : OFF;
-  } else {
-    String strStatus(status.as<const char*>());
-    return (strStatus.equalsIgnoreCase("on") || strStatus.equalsIgnoreCase("true")) ? ON : OFF;
+uint8_t MiLightClient::parseStatus(JsonVariant val) {
+  if (val.isUndefined()) {
+    return 255;
   }
+
+  return parseMilightStatus(val);
 }
 
 void MiLightClient::setRepeatsOverride(size_t repeats) {

+ 2 - 1
lib/MiLight/MiLightClient.h

@@ -110,7 +110,8 @@ public:
   // Clear the repeats override so that the default is used
   void clearRepeatsOverride();
 
-  uint8_t parseStatus(JsonObject object);
+  uint8_t parseStatus(JsonVariant object);
+  JsonVariant extractStatus(JsonObject object);
 
 protected:
   struct cmp_str {

+ 61 - 0
lib/Transitions/ChangeFieldOnFinishTransition.cpp

@@ -0,0 +1,61 @@
+#include <ChangeFieldOnFinishTransition.h>
+#include <MiLightStatus.h>
+
+ChangeFieldOnFinishTransition::Builder::Builder(
+  size_t id,
+  GroupStateField field,
+  uint16_t arg,
+  std::shared_ptr<Transition::Builder> delegate
+)
+  : Transition::Builder(delegate->id, delegate->bulbId, delegate->callback)
+  , delegate(delegate)
+  , field(field)
+  , arg(arg)
+{ }
+
+std::shared_ptr<Transition> ChangeFieldOnFinishTransition::Builder::_build() const {
+  delegate->setPeriod(this->getPeriod());
+  delegate->setNumPeriods(this->getNumPeriods());
+  delegate->setDurationRaw(this->getDuration());
+
+  return std::make_shared<ChangeFieldOnFinishTransition>(
+    delegate->build(),
+    field,
+    arg,
+    delegate->getPeriod()
+  );
+}
+
+ChangeFieldOnFinishTransition::ChangeFieldOnFinishTransition(
+  std::shared_ptr<Transition> delegate,
+  GroupStateField field,
+  uint16_t arg,
+  size_t period
+) : Transition(delegate->id, delegate->bulbId, period, delegate->callback)
+  , delegate(delegate)
+  , field(field)
+  , arg(arg)
+  , changeSent(false)
+{ }
+
+bool ChangeFieldOnFinishTransition::isFinished() {
+  return delegate->isFinished() && changeSent;
+}
+
+void ChangeFieldOnFinishTransition::step() {
+  if (! delegate->isFinished()) {
+    delegate->step();
+  } else {
+    callback(bulbId, field, arg);
+    changeSent = true;
+  }
+}
+
+void ChangeFieldOnFinishTransition::childSerialize(JsonObject& json) {
+  json[F("type")] = F("change_on_finish");
+  json[F("field")] = GroupStateFieldHelpers::getFieldName(field);
+  json[F("value")] = arg;
+
+  JsonObject child = json.createNestedObject(F("child"));
+  delegate->childSerialize(child);
+}

+ 37 - 0
lib/Transitions/ChangeFieldOnFinishTransition.h

@@ -0,0 +1,37 @@
+#include <Transition.h>
+
+#pragma once
+
+class ChangeFieldOnFinishTransition : public Transition {
+public:
+
+  class Builder : public Transition::Builder {
+  public:
+    Builder(size_t id, GroupStateField field, uint16_t arg, std::shared_ptr<Transition::Builder> delgate);
+
+    virtual std::shared_ptr<Transition> _build() const override;
+
+  private:
+    const std::shared_ptr<Transition::Builder> delegate;
+    const GroupStateField field;
+    const uint16_t arg;
+  };
+
+  ChangeFieldOnFinishTransition(
+    std::shared_ptr<Transition> delegate,
+    GroupStateField field,
+    uint16_t arg,
+    size_t period
+  );
+
+  virtual bool isFinished() override;
+
+private:
+  std::shared_ptr<Transition> delegate;
+  const GroupStateField field;
+  const uint16_t arg;
+  bool changeSent;
+
+  virtual void step() override;
+  virtual void childSerialize(JsonObject& json) override;
+};

+ 17 - 1
lib/Transitions/Transition.cpp

@@ -16,6 +16,10 @@ Transition::Builder& Transition::Builder::setDuration(float duration) {
   return *this;
 }
 
+void Transition::Builder::setDurationRaw(size_t duration) {
+  this->duration = duration;
+}
+
 Transition::Builder& Transition::Builder::setPeriod(size_t period) {
   this->period = period;
   return *this;
@@ -26,6 +30,18 @@ Transition::Builder& Transition::Builder::setNumPeriods(size_t numPeriods) {
   return *this;
 }
 
+size_t Transition::Builder::getNumPeriods() const {
+  return this->numPeriods;
+}
+
+size_t Transition::Builder::getDuration() const {
+  return this->duration;
+}
+
+size_t Transition::Builder::getPeriod() const {
+  return this->period;
+}
+
 bool Transition::Builder::isSetDuration() const {
   return this->duration > 0;
 }
@@ -103,8 +119,8 @@ Transition::Transition(
   TransitionFn callback
 ) : id(id)
   , bulbId(bulbId)
-  , period(period)
   , callback(callback)
+  , period(period)
   , lastSent(0)
 { }
 

+ 11 - 6
lib/Transitions/Transition.h

@@ -24,6 +24,8 @@ public:
     Builder& setPeriod(size_t period);
     Builder& setNumPeriods(size_t numPeriods);
 
+    void setDurationRaw(size_t duration);
+
     bool isSetDuration() const;
     bool isSetPeriod() const;
     bool isSetNumPeriods() const;
@@ -32,12 +34,15 @@ public:
     size_t getOrComputeDuration() const;
     size_t getOrComputeNumPeriods() const;
 
+    size_t getDuration() const;
+    size_t getPeriod() const;
+    size_t getNumPeriods() const;
+
     std::shared_ptr<Transition> build();
 
-  protected:
-    size_t id;
+    const size_t id;
     const BulbId& bulbId;
-    TransitionFn callback;
+    const TransitionFn callback;
 
   private:
     size_t duration;
@@ -56,6 +61,7 @@ public:
 
   const size_t id;
   const BulbId bulbId;
+  const TransitionFn callback;
 
   Transition(
     size_t id,
@@ -67,15 +73,14 @@ public:
   void tick();
   virtual bool isFinished() = 0;
   void serialize(JsonObject& doc);
+  virtual void step() = 0;
+  virtual void childSerialize(JsonObject& doc) = 0;
 
   static size_t calculatePeriod(int16_t distance, size_t stepSize, size_t duration);
 
 protected:
   const size_t period;
-  const TransitionFn callback;
   unsigned long lastSent;
 
-  virtual void step() = 0;
-  virtual void childSerialize(JsonObject& doc) = 0;
   static void stepValue(int16_t& current, int16_t end, int16_t stepSize);
 };

+ 26 - 0
lib/Transitions/TransitionController.cpp

@@ -1,7 +1,9 @@
 #include <Transition.h>
 #include <FieldTransition.h>
 #include <ColorTransition.h>
+#include <ChangeFieldOnFinishTransition.h>
 #include <GroupStateField.h>
+#include <MiLightStatus.h>
 
 #include <TransitionController.h>
 #include <LinkedList.h>
@@ -43,6 +45,30 @@ std::shared_ptr<Transition::Builder> TransitionController::buildFieldTransition(
   );
 }
 
+std::shared_ptr<Transition::Builder> TransitionController::buildStatusTransition(const BulbId& bulbId, MiLightStatus status, uint8_t startLevel) {
+  uint16_t value = static_cast<uint16_t>(status);
+  uint16_t startValue = status == ON ? 0 : 100;
+  uint16_t endValue = status == ON ? 100 : 0;
+
+  std::shared_ptr<Transition::Builder> transition;
+
+  if (status == ON) {
+    // Make sure bulb is on before transitioning brightness
+    callback(bulbId, GroupStateField::STATUS, ON);
+
+    transition = buildFieldTransition(bulbId, GroupStateField::LEVEL, startLevel, 100);
+  } else {
+    transition = std::make_shared<ChangeFieldOnFinishTransition::Builder>(
+      currentId++,
+      GroupStateField::STATUS,
+      OFF,
+      buildFieldTransition(bulbId, GroupStateField::LEVEL, startLevel, 0)
+    );
+  }
+
+  return transition;
+}
+
 void TransitionController::addTransition(std::shared_ptr<Transition> transition) {
   activeTransitions.add(transition);
 }

+ 1 - 0
lib/Transitions/TransitionController.h

@@ -16,6 +16,7 @@ public:
 
   std::shared_ptr<Transition::Builder> buildColorTransition(const BulbId& bulbId, const ParsedColor& start, const ParsedColor& end);
   std::shared_ptr<Transition::Builder> buildFieldTransition(const BulbId& bulbId, GroupStateField field, uint16_t start, uint16_t end);
+  std::shared_ptr<Transition::Builder> buildStatusTransition(const BulbId& bulbId, MiLightStatus toStatus, uint8_t startLevel);
 
   void addTransition(std::shared_ptr<Transition> transition);
   void clear();

+ 13 - 0
lib/Types/MiLightStatus.cpp

@@ -0,0 +1,13 @@
+#include <MiLightStatus.h>
+#include <ArduinoJson.h>
+
+MiLightStatus parseMilightStatus(JsonVariant val) {
+  if (val.is<bool>()) {
+    return val.as<bool>() ? ON : OFF;
+  } else if (val.is<uint16_t>()) {
+    return static_cast<MiLightStatus>(val.as<uint16_t>());
+  } else {
+    String strStatus(val.as<const char*>());
+    return (strStatus.equalsIgnoreCase("on") || strStatus.equalsIgnoreCase("true")) ? ON : OFF;
+  }
+}

+ 5 - 1
lib/Types/MiLightStatus.h

@@ -1,6 +1,10 @@
 #pragma once
 
+#include <ArduinoJson.h>
+
 enum MiLightStatus {
   ON = 0,
   OFF = 1
-};
+};
+
+MiLightStatus parseMilightStatus(JsonVariant s);

+ 98 - 0
test/remote/spec/transition_spec.rb

@@ -233,6 +233,104 @@ RSpec.describe 'Transitions' do
       expect(id1_updates.length).to eq(@num_transition_updates)
       expect(id2_updates.length).to eq(@num_transition_updates)
     end
+
+    it 'should assume initial state if one is not provided' do
+      @client.patch_state({status: 'ON', level: 0}, @id_params)
+
+      seen_updates = []
+
+      @mqtt_client.on_update(@id_params) do |id, message|
+        seen_updates << message
+        message['brightness'] == 255
+      end
+
+      @client.schedule_transition(@id_params, @transition_params.reject { |x| x == :start_value }.merge(duration: 2, period: 500))
+
+      @mqtt_client.wait_for_listeners
+
+      expect(seen_updates.map { |x| x['brightness'] }).to eq([0, 64, 128, 191, 255])
+    end
+  end
+
+  context 'status transition' do
+    it 'should transition from off -> on' do
+      seen_updates = {}
+      @client.patch_state({status: 'OFF'}, @id_params)
+
+      @mqtt_client.on_update(@id_params) do |id, message|
+        message.each do |k, v|
+          seen_updates[k] ||= []
+          seen_updates[k] << v
+        end
+        seen_updates['brightness'] && seen_updates['brightness'].last == 255
+      end
+
+      @client.patch_state({status: 'ON', transition: 1.0}, @id_params)
+
+      @mqtt_client.wait_for_listeners
+
+      expect(seen_updates['state']).to eq(['ON'])
+      expect(seen_updates['brightness']).to eq([0, 64, 128, 191, 255])
+    end
+
+    it 'should transition from on -> off' do
+      seen_updates = {}
+      @client.patch_state({status: 'ON', level: 100}, @id_params)
+
+      @mqtt_client.on_update(@id_params) do |id, message|
+        message.each do |k, v|
+          seen_updates[k] ||= []
+          seen_updates[k] << v
+        end
+        seen_updates['state'] == ['OFF']
+      end
+
+      @client.patch_state({status: 'OFF', transition: 1.0}, @id_params)
+
+      @mqtt_client.wait_for_listeners
+
+      expect(seen_updates['state']).to eq(['OFF'])
+      expect(seen_updates['brightness']).to eq([255, 191, 128, 64, 0])
+    end
+
+    it 'should transition from off -> on with known last brightness' do
+      seen_updates = {}
+      @client.patch_state({status: 'ON', brightness: 99}, @id_params)
+      @client.patch_state({status: 'OFF'}, @id_params)
+
+      @mqtt_client.on_update(@id_params) do |id, message|
+        message.each do |k, v|
+          seen_updates[k] ||= []
+          seen_updates[k] << v
+        end
+        seen_updates['brightness'] && seen_updates['brightness'].last == 255
+      end
+
+      @client.patch_state({status: 'ON', transition: 1.0}, @id_params)
+
+      @mqtt_client.wait_for_listeners
+
+      expect(seen_updates['brightness']).to eq([99, 140, 181, 222, 255])
+    end
+
+    it 'should transition from on -> off with known last brightness' do
+      seen_updates = {}
+      @client.patch_state({status: 'ON', brightness: 99}, @id_params)
+
+      @mqtt_client.on_update(@id_params) do |id, message|
+        message.each do |k, v|
+          seen_updates[k] ||= []
+          seen_updates[k] << v
+        end
+        seen_updates['state'] == ['OFF']
+      end
+
+      @client.patch_state({status: 'OFF', transition: 1.0}, @id_params)
+
+      @mqtt_client.wait_for_listeners
+
+      expect(seen_updates['brightness']).to eq([99, 74, 48, 23, 0])
+    end
   end
 
   context 'field support' do