Compare commits

...

50 Commits

Author SHA1 Message Date
e359150d97 Version 0.4.0 2019-02-16 22:05:43 +02:00
93680c981c Resolves #283 Add possibility to restrat HA 2019-02-16 22:04:49 +02:00
e06b66c523 Resolves #259 target_temp_step support for climate 2019-02-16 20:44:41 +02:00
3dea844e1e Resolves #290 Hide pin inputs if code_format is null 2019-02-16 20:33:56 +02:00
62b1af30e0 Resolves #291 some padding issues 2019-02-16 19:59:39 +02:00
e006c4e403 build number 2019-02-10 22:36:30 +02:00
983573388e Remove pull-to-refresh. Add new menu in header. 2019-02-10 22:33:46 +02:00
bdd1dc7e17 Hide light additional controls if state=unavailable 2019-02-10 19:06:07 +02:00
9c1970ee14 build number 2019-02-10 18:26:48 +02:00
d0e0bf3571 Current color for color picker fix 2019-02-10 18:24:54 +02:00
b399357517 Back to old version format 2019-02-10 17:23:57 +02:00
0290cd3a32 Resolves #273 New color picker 2019-02-10 17:15:52 +02:00
d8a1d03179 v.3.14.87 2019-02-09 02:21:20 +02:00
216fad3cb9 Fix entity page padding 2019-02-09 02:21:20 +02:00
fead6ea348 Resolves #143 Camera support 2019-02-09 02:21:20 +02:00
8814687be6 WIP #143 Initial not optimized MJPEG streaming 2019-02-09 02:21:20 +02:00
71c0e2caa0 Change version numeration 2019-02-09 02:21:20 +02:00
1531c41542 Create CODE_OF_CONDUCT.md 2019-02-01 12:55:25 +02:00
bc90d013e8 Build 86 2019-02-01 11:52:09 +02:00
2adfaca0c4 Resolves #286 2019-02-01 11:51:35 +02:00
6cc1a37d9d Resolves #285 2019-02-01 11:49:27 +02:00
4bb616b327 Build number 2019-01-31 21:14:20 +02:00
38219618ba version 0.3.14 2019-01-31 21:05:11 +02:00
6774b53758 Disable unfinished camera support 2019-01-31 20:57:33 +02:00
29a94c882f WIP MJPEG stream handling 2019-01-31 01:04:13 +02:00
5897fa3a99 WIP #143 2019-01-30 00:25:41 +02:00
7af92c2dc9 WIP #143 Camera support 2019-01-29 22:03:08 +02:00
1094177a42 Resolves #282 Trigger button for alarm 2019-01-29 18:51:28 +02:00
5e814e8109 Resolves #204 Alarm panel card support 2019-01-29 15:00:15 +02:00
24c7675fa4 Resolves #142 Alarm control panel support 2019-01-29 11:54:26 +02:00
dc3ca38c78 WIP #142 Alarm control panel 2019-01-28 16:48:49 +02:00
96b528e055 Update README.md 2019-01-28 15:05:19 +02:00
3858036631 Resolves #139 Trigger for automations 2019-01-25 23:48:31 +02:00
19d42ceeb3 Fix names null 2019-01-25 23:30:23 +02:00
a2836a3603 Resolves #257 2019-01-25 23:08:12 +02:00
2a45758a6d Resolves #268 Badges for Lovelace UI 2019-01-25 22:55:41 +02:00
dc1bf4d878 WIP #266 fix icons 2019-01-25 22:45:54 +02:00
e82ba60c4e WIP #266 Card parsing proper error handling and toString for some fields 2019-01-25 22:41:26 +02:00
09199d30e8 Resolves #274 Use Lovelace UI by default 2019-01-25 22:29:16 +02:00
724d32dbe2 Resolves #277 Remove legacy password support 2019-01-25 22:27:13 +02:00
949c8ee44e Resolves #264 Throttle for sending thermostat temperature 2019-01-25 22:19:11 +02:00
1a446d34c7 Resolves #262 Cler 2019-01-25 22:19:11 +02:00
22a5847285 Resolves #270 Current light effect for lights 2019-01-24 12:26:38 +02:00
1c8f770f10 Resolves #121 Markdown card support 2019-01-23 23:34:45 +02:00
be5ea55f6b Fix light color controls appearence issue 2018-12-29 17:44:11 +02:00
c65ade9827 Fix icons for entity button 2018-12-25 11:48:37 +02:00
d3c1422b9e Version 0.3.13 2018-12-15 14:37:55 +02:00
b6ac9f985f Entity state by device class 2018-12-15 14:37:00 +02:00
a59de4b6dc Resolves #255 Refresh UI for newly appeared entity 2018-12-15 14:09:37 +02:00
f507d5df0c Resolves #242 2018-12-14 19:37:49 +02:00
47 changed files with 1387 additions and 480 deletions

76
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,76 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at vyalov.egor@gmail.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq

View File

@ -4,7 +4,7 @@
Home Assistant Android client on Dart with Flutter.
Visit [www.keyboardcrumbs.io](http://www.keyboardcrumbs.io/ha-client) for more info.
Visit [www.vynn.co](https://www.vynn.co/ha-client) for more info.
Join [Google Group](https://groups.google.com/d/forum/ha-client-alpha-testing) to become an alpha tester

View File

@ -31,6 +31,11 @@ keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
android {
compileSdkVersion 27
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
lintOptions {
disable 'InvalidPackage'
}

105
lib/configuration.page.dart Normal file
View File

@ -0,0 +1,105 @@
part of 'main.dart';
class ConfigurationPage extends StatefulWidget {
ConfigurationPage({Key key, this.title}) : super(key: key);
final String title;
@override
_ConfigurationPageState createState() => new _ConfigurationPageState();
}
class ConfigurationItem {
ConfigurationItem({ this.isExpanded: false, this.header, this.body });
bool isExpanded;
final String header;
final Widget body;
}
class _ConfigurationPageState extends State<ConfigurationPage> {
List<ConfigurationItem> _items;
@override
void initState() {
super.initState();
_items = <ConfigurationItem>[
ConfigurationItem(
header: 'General',
body: Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text("Server management", style: TextStyle(fontSize: Sizes.largeFontSize)),
Container(height: Sizes.rowPadding,),
Text("Control your Home Assistant server from HA Client."),
Divider(),
Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FlatServiceButton(
text: "Restart",
serviceName: "restart",
serviceDomain: "homeassistant",
entityId: null,
),
FlatServiceButton(
text: "Stop",
serviceName: "stop",
serviceDomain: "homeassistant",
entityId: null,
),
],
)
],
),
)
)
];
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
Navigator.pop(context);
}),
title: new Text(widget.title),
),
body: ListView(
children: [
new ExpansionPanelList(
expansionCallback: (int index, bool isExpanded) {
setState(() {
_items[index].isExpanded = !_items[index].isExpanded;
});
},
children: _items.map((ConfigurationItem item) {
return new ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return CardHeaderWidget(
name: item.header,
);
},
isExpanded: item.isExpanded,
body: new Container(
child: item.body,
),
);
}).toList(),
),
],
),
);
}
@override
void dispose() {
super.dispose();
}
}

View File

@ -19,7 +19,7 @@ class _EntityViewPageState extends State<EntityViewPage> {
void initState() {
super.initState();
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
TheLogger.debug("State change event handled by entity page: ${event.entityId}");
Logger.d("State change event handled by entity page: ${event.entityId}");
if (event.entityId == widget.entityId) {
setState(() {});
}
@ -46,12 +46,9 @@ class _EntityViewPageState extends State<EntityViewPage> {
// the App.build method, and use it to set our appbar title.
title: new Text(_title),
),
body: Padding(
padding: EdgeInsets.all(10.0),
child: HomeAssistantModel(
homeAssistant: widget.homeAssistant,
child: widget.homeAssistant.entities.get(widget.entityId).buildEntityPageWidget(context)
)
body: HomeAssistantModel(
homeAssistant: widget.homeAssistant,
child: widget.homeAssistant.entities.get(widget.entityId).buildEntityPageWidget(context)
),
);
}

View File

@ -0,0 +1,12 @@
part of '../main.dart';
class AlarmControlPanelEntity extends Entity {
AlarmControlPanelEntity(Map rawData) : super(rawData);
@override
Widget _buildAdditionalControlsForPage(BuildContext context) {
return AlarmControlPanelControlsWidget(
extended: false,
);
}
}

View File

@ -0,0 +1,26 @@
part of '../main.dart';
class AutomationEntity extends Entity {
AutomationEntity(Map rawData) : super(rawData);
@override
Widget _buildStatePart(BuildContext context) {
return SwitchStateWidget();
}
@override
Widget _buildAdditionalControlsForPage(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
FlatServiceButton(
serviceDomain: domain,
entityId: entityId,
text: "TRIGGER",
serviceName: "trigger",
)
],
);
}
}

View File

@ -5,6 +5,11 @@ class ButtonEntity extends Entity {
@override
Widget _buildStatePart(BuildContext context) {
return ButtonStateWidget();
return FlatServiceButton(
entityId: entityId,
serviceDomain: domain,
serviceName: 'turn_on',
text: "EXECUTE",
);
}
}

View File

@ -0,0 +1,19 @@
part of '../main.dart';
class CameraEntity extends Entity {
static const SUPPORT_ON_OFF = 1;
CameraEntity(Map rawData) : super(rawData);
bool get supportOnOff => ((attributes["supported_features"] &
CameraEntity.SUPPORT_ON_OFF) ==
CameraEntity.SUPPORT_ON_OFF);
@override
Widget _buildAdditionalControlsForPage(BuildContext context) {
return CameraControlsWidget(
url: '$homeAssistantWebHost/api/camera_proxy_stream/camera.demo_camera?token=${this.attributes['access_token']}',
);
}
}

View File

@ -80,6 +80,7 @@ class ClimateEntity extends Entity {
double get targetHumidity => _getDoubleAttributeValue('humidity');
double get maxHumidity => _getDoubleAttributeValue('max_humidity');
double get minHumidity => _getDoubleAttributeValue('min_humidity');
double get temperatureStep => _getDoubleAttributeValue('target_temp_step') ?? 0.5;
String get operationMode => attributes['operation_mode'];
String get fanMode => attributes['fan_mode'];
String get swingMode => attributes['swing_mode'];

View File

@ -94,4 +94,5 @@ class CardType {
static const entityButton = "entity-button";
static const conditional = "conditional";
static const alarmPanel = "alarm-panel";
static const markdown = "markdown";
}

View File

@ -12,10 +12,60 @@ class Entity {
"sensor"
];
static Map StateByDeviceClass = {
"battery.on": "Low",
"battery.off": "Normal",
"cold.on": "Cold",
"cold.off": "Normal",
"connectivity.on": "Connected",
"connectivity.off": "Diconnected",
"door.on": "Open",
"door.off": "Closed",
"garage_door.on": "Open",
"garage_door.off": "Closed",
"gas.on": "Detected",
"gas.off": "Clear",
"heat.on": "Hot",
"heat.off": "Normal",
"light.on": "Detected",
"lignt.off": "No light",
"lock.on": "Unlocked",
"lock.off": "Locked",
"moisture.on": "Wet",
"moisture.off": "Dry",
"motion.on": "Detected",
"motion.off": "Clear",
"moving.on": "Moving",
"moving.off": "Stopped",
"occupancy.on": "Occupied",
"occupancy.off": "Clear",
"opening.on": "Open",
"opening.off": "Closed",
"plug.on": "Plugged in",
"plug.off": "Unplugged",
"power.on": "Powered",
"power.off": "No power",
"presence.on": "Home",
"presence.off": "Away",
"problem.on": "Problem",
"problem.off": "OK",
"safety.on": "Unsafe",
"safety.off": "Safe",
"smoke.on": "Detected",
"smoke.off": "Clear",
"sound.on": "Detected",
"sound.off": "Clear",
"vibration.on": "Detected",
"vibration.off": "Clear",
"window.on": "Open",
"window.off": "Closed"
};
Map attributes;
String domain;
String entityId;
String state;
String displayState;
DateTime _lastUpdated;
List<Entity> childEntities = [];
@ -52,6 +102,7 @@ class Entity {
entityId = rawData["entity_id"];
deviceClass = attributes["device_class"];
state = rawData["state"];
displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? state;
_lastUpdated = DateTime.tryParse(rawData["last_updated"]);
}
@ -111,7 +162,10 @@ class Entity {
return EntityModel(
entityWrapper: EntityWrapper(entity: this),
child: EntityPageContainer(children: <Widget>[
DefaultEntityContainer(state: _buildStatePartForPage(context)),
Padding(
padding: EdgeInsets.only(top: Sizes.rowPadding),
child: DefaultEntityContainer(state: _buildStatePartForPage(context)),
),
LastUpdatedWidget(),
Divider(),
_buildAdditionalControlsForPage(context),

View File

@ -33,20 +33,23 @@ class LightEntity extends Entity {
LightEntity.SUPPORT_WHITE_VALUE);
int get brightness => _getIntAttributeValue("brightness");
String get effect => attributes["effect"];
int get colorTemp => _getIntAttributeValue("color_temp");
double get maxMireds => _getDoubleAttributeValue("max_mireds");
double get minMireds => _getDoubleAttributeValue("min_mireds");
Color get color => _getColor();
HSVColor get color => _getColor();
bool get isAdditionalControls => ((attributes["supported_features"] != null) && (attributes["supported_features"] != 0));
List<String> get effectList => getStringListAttributeValue("effect_list");
LightEntity(Map rawData) : super(rawData);
Color _getColor() {
List rgb = attributes["rgb_color"];
HSVColor _getColor() {
List hs = attributes["hs_color"];
try {
if ((rgb != null) && (rgb.length > 0)) {
return Color.fromARGB(255, rgb[0], rgb[1], rgb[2]);
if ((hs != null) && (hs.length > 0)) {
double sat = hs[1]/100;
String ssat = sat.toStringAsFixed(2);
return HSVColor.fromAHSV(1.0, hs[0], double.parse(ssat), 1.0);
} else {
return null;
}
@ -62,7 +65,7 @@ class LightEntity extends Entity {
@override
Widget _buildAdditionalControlsForPage(BuildContext context) {
if (!isAdditionalControls) {
if (!isAdditionalControls || state == EntityState.unavailable) {
return Container(height: 0.0, width: 0.0);
} else {
return LightControlsWidget();

View File

@ -19,7 +19,7 @@ class EntityCollection {
_allEntities.clear();
//views.clear();
TheLogger.debug("Parsing ${rawData.length} Home Assistant entities");
Logger.d("Parsing ${rawData.length} Home Assistant entities");
rawData.forEach((rawEntityData) {
addFromRaw(rawEntityData);
});
@ -47,7 +47,10 @@ class EntityCollection {
case 'lock': {
return LockEntity(rawEntityData);
}
case "automation":
case "automation": {
return AutomationEntity(rawEntityData);
}
case "input_boolean":
case "switch": {
return SwitchEntity(rawEntityData);
@ -83,17 +86,25 @@ class EntityCollection {
case "fan": {
return FanEntity(rawEntityData);
}
case "camera": {
return CameraEntity(rawEntityData);
}
case "alarm_control_panel": {
return AlarmControlPanelEntity(rawEntityData);
}
default: {
return Entity(rawEntityData);
}
}
}
void updateState(Map rawStateData) {
bool updateState(Map rawStateData) {
if (isExist(rawStateData["entity_id"])) {
updateFromRaw(rawStateData["new_state"] ?? rawStateData["old_state"]);
return false;
} else {
addFromRaw(rawStateData["new_state"] ?? rawStateData["old_state"]);
return true;
}
}
@ -101,10 +112,9 @@ class EntityCollection {
_allEntities[entity.entityId] = entity;
}
Entity addFromRaw(Map rawEntityData) {
void addFromRaw(Map rawEntityData) {
Entity entity = _createEntityInstance(rawEntityData);
_allEntities[entity.entityId] = entity;
return entity;
}
void updateFromRaw(Map rawEntityData) {

View File

@ -23,7 +23,22 @@ class BadgeWidget extends StatelessWidget {
);
break;
}
case "sensor":
case "camera":
case "media_player":
case "binary_sensor":
{
badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData(
entityModel.entityWrapper, iconSize, Colors.black);
break;
}
case "device_tracker":
{
badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData(
entityModel.entityWrapper, iconSize, Colors.black);
onBadgeTextValue = entityModel.entityWrapper.entity.state;
break;
}
default:
{
onBadgeTextValue = entityModel.entityWrapper.entity.unitOfMeasurement;
badgeIcon = Center(
@ -37,18 +52,6 @@ class BadgeWidget extends StatelessWidget {
);
break;
}
case "device_tracker":
{
badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData(
entityModel.entityWrapper, iconSize, Colors.black);
onBadgeTextValue = entityModel.entityWrapper.entity.state;
break;
}
default:
{
badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData(
entityModel.entityWrapper, iconSize, Colors.black);
}
}
Widget onBadgeText;
if (onBadgeTextValue == null || onBadgeTextValue.length == 0) {

View File

@ -21,10 +21,13 @@ class EntityAttributesList extends StatelessWidget {
}
});
}
return Column(
children: attrs,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
return Padding(
padding: EdgeInsets.only(bottom: Sizes.rowPadding),
child: Column(
children: attrs,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
)
);
}

View File

@ -0,0 +1,41 @@
part of '../../main.dart';
class FlatServiceButton extends StatelessWidget {
final String serviceDomain;
final String serviceName;
final String entityId;
final String text;
final double fontSize;
FlatServiceButton({
Key key,
@required this.serviceDomain,
@required this.serviceName,
@required this.entityId,
@required this.text,
this.fontSize: Sizes.stateFontSize
}) : super(key: key);
void _setNewState() {
eventBus.fire(new ServiceCallEvent(serviceDomain, serviceName, entityId, null));
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: fontSize*2.5,
child: FlatButton(
onPressed: (() {
_setNewState();
}),
child: Text(
text,
textAlign: TextAlign.right,
style:
new TextStyle(fontSize: fontSize, color: Colors.blue),
),
)
);
}
}

View File

@ -6,7 +6,7 @@ class LastUpdatedWidget extends StatelessWidget {
final entityModel = EntityModel.of(context);
return Padding(
padding: EdgeInsets.fromLTRB(
Sizes.leftWidgetPadding, 0.0, 0.0, 0.0),
Sizes.leftWidgetPadding, Sizes.rowPadding, 0.0, 0.0),
child: Text(
'${entityModel.entityWrapper.entity.lastUpdated}',
textAlign: TextAlign.left,

View File

@ -0,0 +1,101 @@
part of '../../main.dart';
class LightColorPicker extends StatefulWidget {
final HSVColor color;
final onColorSelected;
final double hueStep;
final double saturationStep;
final EdgeInsets padding;
LightColorPicker({this.color, this.onColorSelected, this.hueStep: 15.0, this.saturationStep: 0.2, this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0)});
@override
LightColorPickerState createState() => new LightColorPickerState();
}
class LightColorPickerState extends State<LightColorPicker> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
List<Widget> colorRows = [];
Border border;
bool isSomethingSelected = false;
Logger.d("Current colotfor picker: [${widget.color.hue}, ${widget.color.saturation}]");
for (double saturation = 1.0; saturation >= (0.0 + widget.saturationStep); saturation = double.parse((saturation - widget.saturationStep).toStringAsFixed(2))) {
List<Widget> rowChildren = [];
//Logger.d("$saturation");
double roundedSaturation = double.parse(widget.color.saturation.toStringAsFixed(1));
//Logger.d("Rounded saturation=$roundedSaturation");
for (double hue = 0; hue <= (365 - widget.hueStep);
hue += widget.hueStep) {
bool isExactHue = widget.color.hue.round() == hue;
bool isHueInRange = widget.color.hue.round() > hue && widget.color.hue.round() < (hue+widget.hueStep);
bool isExactSaturation = roundedSaturation == saturation;
bool isSaturationInRange = roundedSaturation > saturation && roundedSaturation < double.parse((saturation+widget.saturationStep).toStringAsFixed(1));
if ((isExactHue || isHueInRange) && (isExactSaturation || isSaturationInRange)) {
//Logger.d("$isExactHue $isHueInRange $isExactSaturation $isSaturationInRange (${saturation+widget.saturationStep})");
border = Border.all(
width: 2.0,
color: Colors.white,
);
isSomethingSelected = true;
} else {
border = null;
}
HSVColor currentColor = HSVColor.fromAHSV(1.0, hue, double.parse(saturation.toStringAsFixed(2)), 1.0);
rowChildren.add(
Flexible(
child: GestureDetector(
child: Container(
height: 40.0,
decoration: BoxDecoration(
color: currentColor.toColor(),
border: border,
),
),
onTap: () => widget.onColorSelected(currentColor),
)
)
);
}
colorRows.add(
Row(
children: rowChildren,
)
);
}
colorRows.add(
Flexible(
child: GestureDetector(
child: Container(
height: 40.0,
decoration: BoxDecoration(
color: Colors.white,
border: isSomethingSelected ? null : Border.all(
width: 2.0,
color: Colors.amber[200],
)
),
),
onTap: () => widget.onColorSelected(HSVColor.fromAHSV(1.0, 30.0, 0.0, 1.0)),
)
)
);
return Padding(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: colorRows,
),
padding: widget.padding,
);
}
}

View File

@ -7,8 +7,8 @@ class ModeSelectorWidget extends StatelessWidget {
final String value;
final double captionFontSize;
final double valueFontSize;
final double bottomPadding;
final onChange;
final EdgeInsets padding;
ModeSelectorWidget({
Key key,
@ -18,45 +18,47 @@ class ModeSelectorWidget extends StatelessWidget {
@required this.onChange,
this.captionFontSize,
this.valueFontSize,
this.bottomPadding
this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0),
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text("$caption", style: TextStyle(
fontSize: captionFontSize ?? Sizes.stateFontSize
)),
Row(
children: <Widget>[
Expanded(
child: ButtonTheme(
alignedDropdown: true,
child: DropdownButton<String>(
value: value,
iconSize: 30.0,
isExpanded: true,
style: TextStyle(
fontSize: valueFontSize ?? Sizes.largeFontSize,
color: Colors.black,
return Padding(
padding: padding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text("$caption", style: TextStyle(
fontSize: captionFontSize ?? Sizes.stateFontSize
)),
Row(
children: <Widget>[
Expanded(
child: ButtonTheme(
alignedDropdown: true,
child: DropdownButton<String>(
value: value,
iconSize: 30.0,
isExpanded: true,
style: TextStyle(
fontSize: valueFontSize ?? Sizes.largeFontSize,
color: Colors.black,
),
hint: Text("Select ${caption.toLowerCase()}"),
items: options.map((String value) {
return new DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
onChanged: (mode) => onChange(mode),
),
hint: Text("Select ${caption.toLowerCase()}"),
items: options.map((String value) {
return new DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
onChanged: (mode) => onChange(mode),
),
),
)
],
),
Container(height: bottomPadding ?? Sizes.rowPadding,)
],
)
],
)
],
),
);
}
}

View File

@ -7,6 +7,7 @@ class ModeSwitchWidget extends StatelessWidget {
final double captionFontSize;
final bool value;
final bool expanded;
final EdgeInsets padding;
ModeSwitchWidget({
Key key,
@ -14,19 +15,23 @@ class ModeSwitchWidget extends StatelessWidget {
@required this.onChange,
this.captionFontSize,
this.value,
this.expanded: true
this.expanded: true,
this.padding: const EdgeInsets.only(left: Sizes.leftWidgetPadding, right: Sizes.rightWidgetPadding)
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
_buildCaption(),
Switch(
onChanged: (value) => onChange(value),
value: value ?? false,
)
],
return Padding(
padding: this.padding,
child: Row(
children: <Widget>[
_buildCaption(),
Switch(
onChanged: (value) => onChange(value),
value: value ?? false,
)
],
)
);
}

View File

@ -10,8 +10,9 @@ class UniversalSlider extends StatelessWidget {
final double min;
final double max;
final double value;
final EdgeInsets padding;
const UniversalSlider({Key key, this.onChanged, this.onChangeEnd, this.leading, this.closing, this.title, this.min, this.max, this.value}) : super(key: key);
const UniversalSlider({Key key, this.onChanged, this.onChangeEnd, this.leading, this.closing, this.title, this.min, this.max, this.value, this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0)}) : super(key: key);
@override
Widget build(BuildContext context) {
@ -33,21 +34,24 @@ class UniversalSlider extends StatelessWidget {
if (closing != null) {
row.add(closing);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(height: Sizes.rowPadding,),
Text(
"$title",
style: TextStyle(fontSize: Sizes.stateFontSize),
),
Container(height: Sizes.rowPadding,),
Row(
mainAxisSize: MainAxisSize.min,
children: row,
),
Container(height: Sizes.rowPadding,)
],
return Padding(
padding: padding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(height: Sizes.rowPadding,),
Text(
"$title",
style: TextStyle(fontSize: Sizes.stateFontSize),
),
Container(height: Sizes.rowPadding,),
Row(
mainAxisSize: MainAxisSize.min,
children: row,
),
Container(height: Sizes.rowPadding,)
],
),
);
}

View File

@ -0,0 +1,262 @@
part of '../../main.dart';
class AlarmControlPanelControlsWidget extends StatefulWidget {
final bool extended;
final List states;
const AlarmControlPanelControlsWidget({Key key, @required this.extended, this.states}) : super(key: key);
@override
_AlarmControlPanelControlsWidgetWidgetState createState() => _AlarmControlPanelControlsWidgetWidgetState();
}
class _AlarmControlPanelControlsWidgetWidgetState extends State<AlarmControlPanelControlsWidget> {
String code = "";
List supportedStates;
@override
void initState() {
super.initState();
supportedStates = widget.states ?? ["arm_home", "arm_away"];
}
void _callService(AlarmControlPanelEntity entity, String service) {
eventBus.fire(new ServiceCallEvent(
entity.domain, service, entity.entityId,
{"code": "$code"}));
setState(() {
code = "";
});
}
void _pinPadHandler(value) {
setState(() {
code += "$value";
});
}
void _pinPadClear() {
setState(() {
code = "";
});
}
void _askToTrigger(AlarmControlPanelEntity entity) {
// flutter defined function
showDialog(
context: context,
builder: (BuildContext context) {
// return object of type Dialog
return AlertDialog(
title: new Text("Are you sure?"),
content: new Text("Are you sure want to trigger alarm ${entity.displayName}?"),
actions: <Widget>[
FlatButton(
child: new Text("Yes"),
onPressed: () {
eventBus.fire(new ServiceCallEvent(entity.domain, "alarm_trigger", entity.entityId, null));
Navigator.of(context).pop();
},
),
FlatButton(
child: new Text("No"),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final AlarmControlPanelEntity entity = entityModel.entityWrapper.entity;
List<Widget> buttons = [];
if (entity.state == EntityState.alarm_disarmed) {
if (supportedStates.contains("arm_home")) {
buttons.add(
RaisedButton(
onPressed: () => _callService(entity, "alarm_arm_home"),
child: Text("ARM HOME"),
)
);
}
if (supportedStates.contains("arm_away")) {
buttons.add(
RaisedButton(
onPressed: () => _callService(entity, "alarm_arm_away"),
child: Text("ARM AWAY"),
)
);
}
if (widget.extended) {
if (supportedStates.contains("arm_night")) {
buttons.add(
RaisedButton(
onPressed: () => _callService(entity, "alarm_arm_night"),
child: Text("ARM NIGHT"),
)
);
}
if (supportedStates.contains("arm_custom_bypass")) {
buttons.add(
RaisedButton(
onPressed: () =>
_callService(entity, "alarm_arm_custom_bypass"),
child: Text("ARM CUSTOM BYPASS"),
)
);
}
}
} else {
buttons.add(
RaisedButton(
onPressed: () => _callService(entity, "alarm_disarm"),
child: Text("DISARM"),
)
);
}
Widget pinPad;
if (entity.attributes["code_format"] == null) {
pinPad = Container(width: 0.0, height: 0.0,);
} else {
pinPad = Padding(
padding: EdgeInsets.only(bottom: Sizes.rowPadding),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Wrap(
spacing: 5.0,
children: <Widget>[
RaisedButton(
onPressed: () => _pinPadHandler("1"),
child: Text("1"),
),
RaisedButton(
onPressed: () => _pinPadHandler("2"),
child: Text("2"),
),
RaisedButton(
onPressed: () => _pinPadHandler("3"),
child: Text("3"),
)
],
),
Wrap(
spacing: 5.0,
children: <Widget>[
RaisedButton(
onPressed: () => _pinPadHandler("4"),
child: Text("4"),
),
RaisedButton(
onPressed: () => _pinPadHandler("5"),
child: Text("5"),
),
RaisedButton(
onPressed: () => _pinPadHandler("6"),
child: Text("6"),
)
],
),
Wrap(
spacing: 5.0,
children: <Widget>[
RaisedButton(
onPressed: () => _pinPadHandler("7"),
child: Text("7"),
),
RaisedButton(
onPressed: () => _pinPadHandler("8"),
child: Text("8"),
),
RaisedButton(
onPressed: () => _pinPadHandler("9"),
child: Text("9"),
)
],
),
Wrap(
spacing: 5.0,
alignment: WrapAlignment.end,
children: <Widget>[
RaisedButton(
onPressed: () => _pinPadHandler("0"),
child: Text("0"),
),
RaisedButton(
onPressed: () => _pinPadClear(),
child: Text("CLEAR"),
)
],
)
],
)
);
}
Widget inputWrapper;
if (entity.attributes["code_format"] == null) {
inputWrapper = Container(width: 0.0, height: 0.0,);
} else {
inputWrapper = Container(
width: 150.0,
child: TextField(
decoration: InputDecoration(
labelText: "Alarm Code"
),
//focusNode: _focusNode,
obscureText: true,
controller: new TextEditingController.fromValue(
new TextEditingValue(
text: code,
selection:
new TextSelection.collapsed(offset: code.length)
)
),
onChanged: (value) {
code = value;
}
)
);
}
Widget buttonsWrapper = Padding(
padding: EdgeInsets.symmetric(vertical: Sizes.rowPadding),
child: Wrap(
alignment: WrapAlignment.center,
spacing: 15.0,
runSpacing: Sizes.rowPadding,
children: buttons
)
);
Widget triggerButton = Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FlatButton(
child: Text(
"TRIGGER",
style: TextStyle(color: Colors.redAccent)
),
onPressed: () => _askToTrigger(entity),
)
]
);
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
widget.extended ? buttonsWrapper : inputWrapper,
widget.extended ? inputWrapper : buttonsWrapper,
widget.extended ? pinPad : triggerButton
]
);
}
}

View File

@ -0,0 +1,115 @@
part of '../../main.dart';
class CameraControlsWidget extends StatefulWidget {
final String url;
CameraControlsWidget({Key key, @required this.url}) : super(key: key);
@override
_CameraControlsWidgetState createState() => _CameraControlsWidgetState();
}
class _CameraControlsWidgetState extends State<CameraControlsWidget> {
@override
void initState() {
super.initState();
_getData();
}
http.Client client;
http.StreamedResponse response;
List<int> binaryImage = [];
String cameraState = "Connecting...";
bool timeToStop = false;
Completer streamCompleter;
void _getData() async {
client = new http.Client(); // create a client to make api calls
http.Request request = new http.Request("GET", Uri.parse(widget.url)); // create get request
Logger.d("[Sending] ==> ${widget.url}");
response = await client.send(request);
setState(() {
cameraState = "Starting...";
});
Logger.d("[Received] <== ${response.headers}");
List<int> primaryBuffer=[];
final int imageSizeStart = 59;
int imageSizeEnd = 0;
int imageStart = 0;
int imageSize = 0;
String strBuffer = "";
streamCompleter = Completer();
response.stream.transform(
StreamTransformer.fromHandlers(
handleData: (data, sink) {
primaryBuffer.addAll(data);
if (primaryBuffer.length >= 66) {
for (int i = imageSizeStart; i < primaryBuffer.length - 4; i++) {
strBuffer = utf8.decode(
primaryBuffer.sublist(i, i + 4), allowMalformed: true);
if (strBuffer == "\r\n\r\n") {
imageSizeEnd = i;
imageStart = i + 4;
break;
}
}
imageSize = int.tryParse(utf8.decode(
primaryBuffer.sublist(imageSizeStart, imageSizeEnd),
allowMalformed: true));
if (imageSize != null && primaryBuffer.length >= imageStart + imageSize + 2) {
sink.add(
primaryBuffer.sublist(imageStart, imageStart + imageSize));
primaryBuffer.removeRange(0, imageStart + imageSize + 2);
}
}
if (timeToStop) {
sink?.close();
streamCompleter.complete();
}
},
handleError: (error, stack, sink) {
Logger.e("Error parsing MJPEG stream: $error");
},
handleDone: (sink) {
sink?.close();
},
)
).listen((d) {
setState(() {
binaryImage = d;
});
});
}
@override
Widget build(BuildContext context) {
if (binaryImage.isEmpty) {
return Column(
children: <Widget>[
Text("$cameraState")
],
);
} else {
return Column(
children: <Widget>[
Image.memory(Uint8List.fromList(binaryImage), gaplessPlayback: true),
],
);
}
}
@override
void dispose() {
super.dispose();
timeToStop = true;
if (streamCompleter != null && !streamCompleter.isCompleted) {
streamCompleter.future.then((_) {
client?.close();
});
} else {
client?.close();
}
}
}

View File

@ -13,6 +13,8 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
bool _showPending = false;
bool _changedHere = false;
Timer _resetTimer;
Timer _tempThrottleTimer;
Timer _targetTempThrottleTimer;
double _tmpTemperature = 0.0;
double _tmpTargetLow = 0.0;
double _tmpTargetHigh = 0.0;
@ -40,52 +42,68 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
_changedHere = false;
}
void _temperatureUp(ClimateEntity entity, double step) {
_tmpTemperature = ((_tmpTemperature + step) <= entity.maxTemp) ? _tmpTemperature + step : entity.maxTemp;
void _temperatureUp(ClimateEntity entity) {
_tmpTemperature = ((_tmpTemperature + entity.temperatureStep) <= entity.maxTemp) ? _tmpTemperature + entity.temperatureStep : entity.maxTemp;
_setTemperature(entity);
}
void _temperatureDown(ClimateEntity entity, double step) {
_tmpTemperature = ((_tmpTemperature - step) >= entity.minTemp) ? _tmpTemperature - step : entity.minTemp;
void _temperatureDown(ClimateEntity entity) {
_tmpTemperature = ((_tmpTemperature - entity.temperatureStep) >= entity.minTemp) ? _tmpTemperature - entity.temperatureStep : entity.minTemp;
_setTemperature(entity);
}
void _targetLowUp(ClimateEntity entity, double step) {
_tmpTargetLow = ((_tmpTargetLow + step) <= entity.maxTemp) ? _tmpTargetLow + step : entity.maxTemp;
void _targetLowUp(ClimateEntity entity) {
_tmpTargetLow = ((_tmpTargetLow + entity.temperatureStep) <= entity.maxTemp) ? _tmpTargetLow + entity.temperatureStep : entity.maxTemp;
_setTargetTemp(entity);
}
void _targetLowDown(ClimateEntity entity, double step) {
_tmpTargetLow = ((_tmpTargetLow - step) >= entity.minTemp) ? _tmpTargetLow - step : entity.minTemp;
void _targetLowDown(ClimateEntity entity) {
_tmpTargetLow = ((_tmpTargetLow - entity.temperatureStep) >= entity.minTemp) ? _tmpTargetLow - entity.temperatureStep : entity.minTemp;
_setTargetTemp(entity);
}
void _targetHighUp(ClimateEntity entity, double step) {
_tmpTargetHigh = ((_tmpTargetHigh + step) <= entity.maxTemp) ? _tmpTargetHigh + step : entity.maxTemp;
void _targetHighUp(ClimateEntity entity) {
_tmpTargetHigh = ((_tmpTargetHigh + entity.temperatureStep) <= entity.maxTemp) ? _tmpTargetHigh + entity.temperatureStep : entity.maxTemp;
_setTargetTemp(entity);
}
void _targetHighDown(ClimateEntity entity, double step) {
_tmpTargetHigh = ((_tmpTargetHigh - step) >= entity.minTemp) ? _tmpTargetHigh - step : entity.minTemp;
void _targetHighDown(ClimateEntity entity) {
_tmpTargetHigh = ((_tmpTargetHigh - entity.temperatureStep) >= entity.minTemp) ? _tmpTargetHigh - entity.temperatureStep : entity.minTemp;
_setTargetTemp(entity);
}
void _setTemperature(ClimateEntity entity) {
if (_tempThrottleTimer!=null) {
_tempThrottleTimer.cancel();
}
setState(() {
_tmpTemperature = double.parse(_tmpTemperature.toStringAsFixed(1));
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_temperature", entity.entityId,{"temperature": "${_tmpTemperature.toStringAsFixed(1)}"}));
_resetStateTimer(entity);
_tmpTemperature = double.parse(_tmpTemperature.toStringAsFixed(1));
});
_tempThrottleTimer = Timer(Duration(seconds: 2), () {
setState(() {
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_temperature", entity.entityId,{"temperature": "${_tmpTemperature.toStringAsFixed(1)}"}));
_resetStateTimer(entity);
});
});
}
void _setTargetTemp(ClimateEntity entity) {
if (_targetTempThrottleTimer!=null) {
_targetTempThrottleTimer.cancel();
}
setState(() {
_changedHere = true;
_tmpTargetLow = double.parse(_tmpTargetLow.toStringAsFixed(1));
_tmpTargetHigh = double.parse(_tmpTargetHigh.toStringAsFixed(1));
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_temperature", entity.entityId,{"target_temp_high": "${_tmpTargetHigh.toStringAsFixed(1)}", "target_temp_low": "${_tmpTargetLow.toStringAsFixed(1)}"}));
_resetStateTimer(entity);
});
_targetTempThrottleTimer = Timer(Duration(seconds: 2), () {
setState(() {
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_temperature", entity.entityId,{"target_temp_high": "${_tmpTargetHigh.toStringAsFixed(1)}", "target_temp_low": "${_tmpTargetLow.toStringAsFixed(1)}"}));
_resetStateTimer(entity);
});
});
}
@ -167,7 +185,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
final entityModel = EntityModel.of(context);
final ClimateEntity entity = entityModel.entityWrapper.entity;
if (_changedHere) {
_showPending = (_tmpTemperature != entity.temperature);
_showPending = (_tmpTemperature != entity.temperature || _tmpTargetHigh != entity.targetHigh || _tmpTargetLow != entity.targetLow);
_changedHere = false;
} else {
_resetTimer?.cancel();
@ -278,10 +296,8 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
TemperatureControlWidget(
value: _tmpTemperature,
fontColor: _showPending ? Colors.red : Colors.black,
onLargeDec: () => _temperatureDown(entity, 0.5),
onLargeInc: () => _temperatureUp(entity, 0.5),
onSmallDec: () => _temperatureDown(entity, 0.1),
onSmallInc: () => _temperatureUp(entity, 0.1),
onDec: () => _temperatureDown(entity),
onInc: () => _temperatureUp(entity),
)
],
);
@ -297,10 +313,8 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
TemperatureControlWidget(
value: _tmpTargetLow,
fontColor: _showPending ? Colors.red : Colors.black,
onLargeDec: () => _targetLowDown(entity, 0.5),
onLargeInc: () => _targetLowUp(entity, 0.5),
onSmallDec: () => _targetLowDown(entity, 0.1),
onSmallInc: () => _targetLowUp(entity, 0.1),
onDec: () => _targetLowDown(entity),
onInc: () => _targetLowUp(entity),
),
Expanded(
child: Container(height: 10.0),
@ -312,10 +326,8 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
TemperatureControlWidget(
value: _tmpTargetHigh,
fontColor: _showPending ? Colors.red : Colors.black,
onLargeDec: () => _targetHighDown(entity, 0.5),
onLargeInc: () => _targetHighUp(entity, 0.5),
onSmallDec: () => _targetHighDown(entity, 0.1),
onSmallInc: () => _targetHighUp(entity, 0.1),
onDec: () => _targetHighDown(entity),
onInc: () => _targetHighUp(entity),
)
);
}
@ -401,18 +413,14 @@ class TemperatureControlWidget extends StatelessWidget {
final double value;
final double fontSize;
final Color fontColor;
final onSmallInc;
final onLargeInc;
final onSmallDec;
final onLargeDec;
final onInc;
final onDec;
TemperatureControlWidget(
{Key key,
@required this.value,
@required this.onSmallInc,
@required this.onSmallDec,
@required this.onLargeInc,
@required this.onLargeDec,
@required this.onInc,
@required this.onDec,
this.fontSize,
this.fontColor})
: super(key: key);
@ -435,29 +443,13 @@ class TemperatureControlWidget extends StatelessWidget {
icon: Icon(MaterialDesignIcons.createIconDataFromIconName(
'mdi:chevron-up')),
iconSize: 30.0,
onPressed: () => onSmallInc(),
onPressed: () => onInc(),
),
IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName(
'mdi:chevron-down')),
iconSize: 30.0,
onPressed: () => onSmallDec(),
)
],
),
Column(
children: <Widget>[
IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName(
'mdi:chevron-double-up')),
iconSize: 30.0,
onPressed: () => onLargeInc(),
),
IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName(
'mdi:chevron-double-down')),
iconSize: 30.0,
onPressed: () => onLargeDec(),
onPressed: () => onDec(),
)
],
)

View File

@ -10,16 +10,16 @@ class LightControlsWidget extends StatefulWidget {
class _LightControlsWidgetState extends State<LightControlsWidget> {
int _tmpBrightness;
int _tmpColorTemp;
Color _tmpColor;
int _tmpColorTemp = 0;
HSVColor _tmpColor = HSVColor.fromAHSV(1.0, 30.0, 0.0, 1.0);
bool _changedHere = false;
String _tmpEffect;
void _resetState(LightEntity entity) {
_tmpBrightness = entity.brightness ?? 0;
_tmpColorTemp = entity.colorTemp;
_tmpColor = entity.color;
_tmpEffect = null;
_tmpColorTemp = entity.colorTemp ?? entity.minMireds?.toInt();
_tmpColor = entity.color ?? _tmpColor;
_tmpEffect = entity.effect;
}
void _setBrightness(LightEntity entity, double value) {
@ -48,20 +48,14 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
});
}
void _setColor(LightEntity entity, Color color) {
void _setColor(LightEntity entity, HSVColor color) {
setState(() {
_tmpColor = color;
_changedHere = true;
TheLogger.debug( "Color: [${color.red}, ${color.green}, ${color.blue}]");
if ((color == Colors.black) || ((color.red == color.green) && (color.green == color.blue))) {
eventBus.fire(new ServiceCallEvent(
entity.domain, "turn_off", entity.entityId,
null));
} else {
eventBus.fire(new ServiceCallEvent(
entity.domain, "turn_on", entity.entityId,
{"rgb_color": [color.red, color.green, color.blue]}));
}
Logger.d( "HS Color: [${color.hue}, ${color.saturation}]");
eventBus.fire(new ServiceCallEvent(
entity.domain, "turn_on", entity.entityId,
{"hs_color": [color.hue, color.saturation*100]}));
});
}
@ -98,7 +92,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
}
Widget _buildBrightnessControl(LightEntity entity) {
if ((entity.supportBrightness) && (_tmpBrightness != null) && (entity.state != EntityState.unavailable)) {
if ((entity.supportBrightness) && (_tmpBrightness != null)) {
return UniversalSlider(
onChanged: (value) {
setState(() {
@ -109,7 +103,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
min: 0.0,
max: 255.0,
onChangeEnd: (value) => _setBrightness(entity, value),
value: _tmpBrightness.toDouble(),
value: _tmpBrightness == null ? 0.0 : _tmpBrightness.toDouble(),
leading: Icon(Icons.brightness_5),
title: "Brightness",
);
@ -119,11 +113,11 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
}
Widget _buildColorTempControl(LightEntity entity) {
if ((entity.supportColorTemp) && (_tmpColorTemp != null)) {
if (entity.supportColorTemp) {
return UniversalSlider(
title: "Color temperature",
leading: Text("Cold", style: TextStyle(color: Colors.lightBlue),),
value: _tmpColorTemp.toDouble(),
value: _tmpColorTemp == null ? entity.maxMireds : _tmpColorTemp.toDouble(),
onChangeEnd: (value) => _setColorTemp(entity, value),
max: entity.maxMireds,
min: entity.minMireds,
@ -141,26 +135,10 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
}
Widget _buildColorControl(LightEntity entity) {
if ((entity.supportColor) && (entity.color != null)) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Container(height: Sizes.rowPadding,),
RaisedButton(
onPressed: () => _showColorPicker(entity),
color: _tmpColor ?? Colors.black45,
child: Text(
"COLOR",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 50.0,
fontWeight: FontWeight.bold,
color: Colors.black12,
),
),
),
Container(height: 2*Sizes.rowPadding,),
],
if (entity.supportColor) {
return LightColorPicker(
color: _tmpColor,
onColorSelected: (color) => _setColor(entity, color),
);
} else {
return Container(width: 0.0, height: 0.0);
@ -174,15 +152,8 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
return AlertDialog(
titlePadding: EdgeInsets.all(0.0),
contentPadding: EdgeInsets.all(0.0),
content: SingleChildScrollView(
child: MaterialPicker(
pickerColor: _tmpColor,
onColorChanged: (color) {
_setColor(entity, color);
Navigator.of(context).pop();
},
enableLabel: true,
),
content: LightColorPicker(
color: _tmpColor,
),
);
},

View File

@ -120,12 +120,12 @@ class MediaPlayerPlaybackControls extends StatelessWidget {
void _setPower(MediaPlayerEntity entity) {
if (entity.state != EntityState.unavailable && entity.state != EntityState.unknown) {
if (entity.state == EntityState.off) {
TheLogger.debug("${entity.entityId} turn_on");
Logger.d("${entity.entityId} turn_on");
eventBus.fire(new ServiceCallEvent(
entity.domain, "turn_on", entity.entityId,
null));
} else {
TheLogger.debug("${entity.entityId} turn_off");
Logger.d("${entity.entityId} turn_off");
eventBus.fire(new ServiceCallEvent(
entity.domain, "turn_off", entity.entityId,
null));
@ -134,7 +134,7 @@ class MediaPlayerPlaybackControls extends StatelessWidget {
}
void _callAction(MediaPlayerEntity entity, String action) {
TheLogger.debug("${entity.entityId} $action");
Logger.d("${entity.entityId} $action");
eventBus.fire(new ServiceCallEvent(
entity.domain, "$action", entity.entityId,
null));

View File

@ -23,6 +23,15 @@ class EntityColor {
"cool": Colors.lightBlue,
EntityState.unavailable: Colors.black26,
EntityState.unknown: Colors.black26,
EntityState.alarm_disarmed: Colors.green,
EntityState.alarm_armed_away: Colors.redAccent,
EntityState.alarm_armed_custom_bypass: Colors.redAccent,
EntityState.alarm_armed_home: Colors.redAccent,
EntityState.alarm_armed_night: Colors.redAccent,
EntityState.alarm_triggered: Colors.redAccent,
EntityState.alarm_arming: Colors.amber,
EntityState.alarm_disarming: Colors.amber,
EntityState.alarm_pending: Colors.amber,
};
static Color stateColor(String state) {

View File

@ -94,11 +94,11 @@ class _CombinedHistoryChartWidgetState extends State<CombinedHistoryChartWidget>
}
List<charts.Series<EntityHistoryMoment, DateTime>> _parseHistory() {
TheLogger.debug(" parsing history...");
Logger.d(" parsing history...");
Map<String, List<EntityHistoryMoment>> numericDataLists = {};
int colorIdCounter = 0;
widget.config.numericAttributesToShow.forEach((String attrName) {
TheLogger.debug(" parsing attribute $attrName");
Logger.d(" parsing attribute $attrName");
List<EntityHistoryMoment> data = [];
DateTime now = DateTime.now();
for (var i = 0; i < widget.rawHistory.length; i++) {
@ -152,7 +152,7 @@ class _CombinedHistoryChartWidgetState extends State<CombinedHistoryChartWidget>
}
List<charts.Series<EntityHistoryMoment, DateTime>> result = [];
numericDataLists.forEach((attrName, dataList) {
TheLogger.debug(" adding ${dataList.length} data values");
Logger.d(" adding ${dataList.length} data values");
result.add(
new charts.Series<EntityHistoryMoment, DateTime>(
id: "value",

View File

@ -42,7 +42,7 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
void _loadHistory(HomeAssistant ha, String entityId) {
DateTime now = DateTime.now();
if (_historyLastUpdated != null) {
TheLogger.debug("History was updated ${now.difference(_historyLastUpdated).inSeconds} seconds ago");
Logger.d("History was updated ${now.difference(_historyLastUpdated).inSeconds} seconds ago");
}
if (_historyLastUpdated == null || now.difference(_historyLastUpdated).inSeconds > 30) {
_historyLastUpdated = now;
@ -52,7 +52,7 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
_needToUpdateHistory = false;
});
}).catchError((e) {
TheLogger.error("Error loading $entityId history: $e");
Logger.e("Error loading $entityId history: $e");
setState(() {
_history = [];
_needToUpdateHistory = false;
@ -122,7 +122,7 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
}
default: {
TheLogger.debug(" Simple selected as default");
Logger.d(" Simple selected as default");
return SimpleStateHistoryChartWidget(
rawHistory: _history,
);

View File

@ -1,27 +0,0 @@
part of '../../main.dart';
class ButtonStateWidget extends StatelessWidget {
void _setNewState(Entity entity) {
eventBus.fire(new ServiceCallEvent(entity.domain, "turn_on", entity.entityId, null));
}
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
return SizedBox(
height: 34.0,
child: FlatButton(
onPressed: (() {
_setNewState(entityModel.entityWrapper.entity);
}),
child: Text(
"EXECUTE",
textAlign: TextAlign.right,
style:
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
),
)
);
}
}

View File

@ -54,7 +54,7 @@ class DateTimeStateWidget extends StatelessWidget {
}
});
} else {
TheLogger.warning( "${entity.entityId} has no date and no time");
Logger.w( "${entity.entityId} has no date and no time");
}
}

View File

@ -12,7 +12,7 @@ class SimpleEntityState extends StatelessWidget {
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
String state = entityModel.entityWrapper.entity.state ?? "";
String state = entityModel.entityWrapper.entity.displayState ?? "";
state = state.replaceAll("\n", "").replaceAll("\t", " ").trim();
while (state.contains(" ")){
state = state.replaceAll(" ", " ");

View File

@ -85,7 +85,7 @@ class _TextInputStateWidgetState extends State<TextInputStateWidget> {
}),
);
} else {
TheLogger.warning( "Unsupported input mode for ${entity.entityId}");
Logger.w( "Unsupported input mode for ${entity.entityId}");
return SimpleEntityState();
}
}

View File

@ -3,7 +3,6 @@ part of 'main.dart';
class HomeAssistant {
String _webSocketAPIEndpoint;
String _password;
String _authType;
bool _useLovelace = false;
IOWebSocketChannel _hassioChannel;
@ -56,21 +55,20 @@ class HomeAssistant {
_messageQueue = SendMessageQueue(messageExpirationTime);
}
void updateSettings(String url, String password, String authType, bool useLovelace) {
void updateSettings(String url, String password, bool useLovelace) {
_webSocketAPIEndpoint = url;
_password = password;
_authType = authType;
_useLovelace = useLovelace;
TheLogger.debug( "Use lovelace is $_useLovelace");
Logger.d( "Use lovelace is $_useLovelace");
}
Future fetch() {
if ((_fetchCompleter != null) && (!_fetchCompleter.isCompleted)) {
TheLogger.warning("Previous fetch is not complited");
Logger.w("Previous fetch is not complited");
} else {
_fetchCompleter = new Completer();
_fetchTimer = Timer(fetchTimeout, () {
TheLogger.error( "Data fetching timeout");
Logger.e( "Data fetching timeout");
disconnect().then((_) {
_completeFetching({
"errorCode": 9,
@ -90,7 +88,7 @@ class HomeAssistant {
disconnect() async {
if ((_hassioChannel != null) && (_hassioChannel.closeCode == null) && (_hassioChannel.sink != null)) {
await _hassioChannel.sink.close().timeout(Duration(seconds: 3),
onTimeout: () => TheLogger.debug( "Socket sink closed")
onTimeout: () => Logger.d( "Socket sink closed")
);
await _socketSubscription.cancel();
_hassioChannel = null;
@ -100,15 +98,15 @@ class HomeAssistant {
Future _connection() {
if ((_connectionCompleter != null) && (!_connectionCompleter.isCompleted)) {
TheLogger.debug("Previous connection is not complited");
Logger.d("Previous connection is not complited");
} else {
if ((_hassioChannel == null) || (_hassioChannel.closeCode != null)) {
_connectionCompleter = new Completer();
autoReconnect = false;
disconnect().then((_){
TheLogger.debug( "Socket connecting...");
Logger.d( "Socket connecting...");
_connectionTimer = Timer(connectTimeout, () {
TheLogger.error( "Socket connection timeout");
Logger.e( "Socket connection timeout");
_handleSocketError(null);
});
if (_socketSubscription != null) {
@ -131,15 +129,15 @@ class HomeAssistant {
}
void _handleSocketClose() {
TheLogger.debug("Socket disconnected. Automatic reconnect is $autoReconnect");
Logger.d("Socket disconnected. Automatic reconnect is $autoReconnect");
if (autoReconnect) {
_reconnect();
}
}
void _handleSocketError(e) {
TheLogger.error("Socket stream Error: $e");
TheLogger.debug("Automatic reconnect is $autoReconnect");
Logger.e("Socket stream Error: $e");
Logger.d("Automatic reconnect is $autoReconnect");
if (autoReconnect) {
_reconnect();
} else {
@ -186,7 +184,7 @@ class HomeAssistant {
_fetchCompleter.completeError(error);
} else {
autoReconnect = true;
TheLogger.debug( "Fetch complete successful");
Logger.d( "Fetch complete successful");
_fetchCompleter.complete();
}
}
@ -213,14 +211,14 @@ class HomeAssistant {
_handleMessage(String message) {
var data = json.decode(message);
if (data["type"] == "auth_required") {
_sendAuthMessageRaw('{"type": "auth","$_authType": "$_password"}');
_sendAuthMessageRaw('{"type": "auth","access_token": "$_password"}');
} else if (data["type"] == "auth_ok") {
_completeConnecting(null);
_sendSubscribe();
} else if (data["type"] == "auth_invalid") {
_completeConnecting({"errorCode": 6, "errorMessage": "${data["message"]}"});
} else if (data["type"] == "result") {
TheLogger.debug("[Received] <== id:${data["id"]}, ${data['success'] ? 'success' : 'error'}");
Logger.d("[Received] <== id:${data["id"]}, ${data['success'] ? 'success' : 'error'}");
if (data["id"] == _configMessageId) {
_parseConfig(data);
} else if (data["id"] == _statesMessageId) {
@ -234,15 +232,15 @@ class HomeAssistant {
}
} else if (data["type"] == "event") {
if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) {
TheLogger.debug("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}");
Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}");
_handleEntityStateChange(data["event"]["data"]);
} else if (data["event"] != null) {
TheLogger.warning("Unhandled event type: ${data["event"]["event_type"]}");
Logger.w("Unhandled event type: ${data["event"]["event_type"]}");
} else {
TheLogger.error("Event is null: $message");
Logger.e("Event is null: $message");
}
} else {
TheLogger.warning("Unknown message type: $message");
Logger.w("Unknown message type: $message");
}
}
@ -302,7 +300,7 @@ class HomeAssistant {
}
void _sendAuthMessageRaw(String message) {
TheLogger.debug( "[Sending] ==> auth request");
Logger.d( "[Sending] ==> auth request");
_hassioChannel.sink.add(message);
}
@ -311,11 +309,11 @@ class HomeAssistant {
if (queued) _messageQueue.add(message);
_connection().then((r) {
_messageQueue.getActualMessages().forEach((message){
TheLogger.debug( "[Sending queued] ==> $message");
Logger.d( "[Sending queued] ==> $message");
_hassioChannel.sink.add(message);
});
if (!queued) {
TheLogger.debug( "[Sending] ==> $message");
Logger.d( "[Sending] ==> $message");
_hassioChannel.sink.add(message);
}
sendCompleter.complete();
@ -367,8 +365,10 @@ class HomeAssistant {
void _handleEntityStateChange(Map eventData) {
//TheLogger.debug( "New state for ${eventData['entity_id']}");
Map data = Map.from(eventData);
entities.updateState(data);
eventBus.fire(new StateChangedEvent(data["entity_id"], null));
eventBus.fire(new StateChangedEvent(
entityId: data["entity_id"],
needToRebuildUI: entities.updateState(data)
));
}
void _parseConfig(Map data) {
@ -385,7 +385,7 @@ class HomeAssistant {
_userName = data["result"]["name"];
} else {
_userName = null;
TheLogger.warning("There was an error getting current user: $data");
Logger.w("There was an error getting current user: $data");
}
_userInfoCompleter.complete();
}
@ -398,25 +398,35 @@ class HomeAssistant {
if (response["success"] == true) {
_rawLovelaceData = response["result"];
} else {
TheLogger.error("There was an error getting Lovelace config: $response");
Logger.e("There was an error getting Lovelace config: $response");
_rawLovelaceData = null;
}
_lovelaceCompleter.complete();
}
void _parseLovelace() {
TheLogger.debug("--Title: ${_rawLovelaceData["title"]}");
Logger.d("--Title: ${_rawLovelaceData["title"]}");
ui.title = _rawLovelaceData["title"];
int viewCounter = 0;
TheLogger.debug("--Views count: ${_rawLovelaceData['views'].length}");
Logger.d("--Views count: ${_rawLovelaceData['views'].length}");
_rawLovelaceData["views"].forEach((rawView){
TheLogger.debug("----view id: ${rawView['id']}");
Logger.d("----view id: ${rawView['id']}");
HAView view = HAView(
count: viewCounter,
id: "${rawView['id']}",
name: rawView['title'],
iconName: rawView['icon']
);
if (rawView['badges'] != null && rawView['badges'] is List) {
rawView['badges'].forEach((entity) {
if (entities.isExist(entity)) {
Entity e = entities.get(entity);
view.badges.add(e);
}
});
}
view.cards.addAll(_createLovelaceCards(rawView["cards"] ?? []));
ui.views.add(
view
@ -428,63 +438,78 @@ class HomeAssistant {
List<HACard> _createLovelaceCards(List rawCards) {
List<HACard> result = [];
rawCards.forEach((rawCard){
bool isThereCardOptionsInside = rawCard["card"] != null;
HACard card = HACard(
id: "card",
name: isThereCardOptionsInside ? rawCard["card"]["title"] ?? rawCard["card"]["name"] : rawCard["title"] ?? rawCard["name"],
type: isThereCardOptionsInside ? rawCard["card"]['type'] : rawCard['type'],
columnsCount: isThereCardOptionsInside ? rawCard["card"]['columns'] ?? 4 : rawCard['columns'] ?? 4,
showName: isThereCardOptionsInside ? rawCard["card"]['show_name'] ?? true : rawCard['show_name'] ?? true,
showState: isThereCardOptionsInside ? rawCard["card"]['show_state'] ?? true : rawCard['show_state'] ?? true,
showEmpty: rawCard['show_empty'] ?? true,
stateFilter: rawCard['state_filter'] ?? []
);
if (rawCard["cards"] != null) {
card.childCards = _createLovelaceCards(rawCard["cards"]);
}
rawCard["entities"]?.forEach((rawEntity) {
if (rawEntity is String) {
if (entities.isExist(rawEntity)) {
card.entities.add(EntityWrapper(entity: entities.get(rawEntity)));
try {
bool isThereCardOptionsInside = rawCard["card"] != null;
HACard card = HACard(
id: "card",
name: isThereCardOptionsInside ? rawCard["card"]["title"] ??
rawCard["card"]["name"] : rawCard["title"] ?? rawCard["name"],
type: isThereCardOptionsInside
? rawCard["card"]['type']
: rawCard['type'],
columnsCount: isThereCardOptionsInside
? rawCard["card"]['columns'] ?? 4
: rawCard['columns'] ?? 4,
showName: isThereCardOptionsInside ? rawCard["card"]['show_name'] ??
true : rawCard['show_name'] ?? true,
showState: isThereCardOptionsInside
? rawCard["card"]['show_state'] ?? true
: rawCard['show_state'] ?? true,
showEmpty: rawCard['show_empty'] ?? true,
stateFilter: rawCard['state_filter'] ?? [],
states: rawCard['states'],
content: rawCard['content']
);
if (rawCard["cards"] != null) {
card.childCards = _createLovelaceCards(rawCard["cards"]);
}
rawCard["entities"]?.forEach((rawEntity) {
if (rawEntity is String) {
if (entities.isExist(rawEntity)) {
card.entities.add(EntityWrapper(entity: entities.get(rawEntity)));
}
} else {
if (entities.isExist(rawEntity["entity"])) {
Entity e = entities.get(rawEntity["entity"]);
card.entities.add(
EntityWrapper(
entity: e,
displayName: rawEntity["name"],
icon: rawEntity["icon"],
uiAction: EntityUIAction(rawEntityData: rawEntity)
)
);
}
}
} else {
if (entities.isExist(rawEntity["entity"])) {
Entity e = entities.get(rawEntity["entity"]);
card.entities.add(
EntityWrapper(
entity: e,
displayName: rawEntity["name"],
icon: rawEntity["icon"],
uiAction: EntityUIAction(rawEntityData: rawEntity)
)
);
});
if (rawCard["entity"] != null) {
var en = rawCard["entity"];
if (en is String) {
if (entities.isExist(en)) {
Entity e = entities.get(en);
card.linkedEntityWrapper = EntityWrapper(
entity: e,
icon: rawCard["icon"],
displayName: rawCard["name"],
uiAction: EntityUIAction(rawEntityData: rawCard)
);
}
} else {
if (entities.isExist(en["entity"])) {
Entity e = entities.get(en["entity"]);
card.linkedEntityWrapper = EntityWrapper(
entity: e,
icon: en["icon"],
displayName: en["name"],
uiAction: EntityUIAction(rawEntityData: rawCard)
);
}
}
}
});
if (rawCard["entity"] != null) {
var en = rawCard["entity"];
if (en is String) {
if (entities.isExist(en)) {
Entity e = entities.get(en);
card.linkedEntityWrapper = EntityWrapper(
entity: e,
uiAction: EntityUIAction(rawEntityData: rawCard)
);
}
} else {
if (entities.isExist(en["entity"])) {
Entity e = entities.get(en["entity"]);
card.linkedEntityWrapper = EntityWrapper(
entity: e,
icon: en["icon"],
displayName: en["name"],
uiAction: EntityUIAction(rawEntityData: rawCard)
);
}
}
result.add(card);
} catch (e) {
Logger.e("There was an error parsing card: ${e.toString()}");
}
result.add(card);
});
return result;
}
@ -501,10 +526,10 @@ class HomeAssistant {
void _createUI() {
ui = HomeAssistantUI();
if ((_useLovelace) && (_rawLovelaceData != null)) {
TheLogger.debug("Creating Lovelace UI");
Logger.d("Creating Lovelace UI");
_parseLovelace();
} else {
TheLogger.debug("Creating group-based UI");
Logger.d("Creating group-based UI");
int viewCounter = 0;
if (!entities.hasDefaultView) {
HAView view = HAView(
@ -543,22 +568,15 @@ class HomeAssistant {
//String endTime = formatDate(now, [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
String startTime = formatDate(now.subtract(Duration(hours: 24)), [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
String url = "$homeAssistantWebHost/api/history/period/$startTime?&filter_entity_id=$entityId";
TheLogger.debug("[Sending] ==> $url");
Logger.d("[Sending] ==> $url");
http.Response historyResponse;
if (_authType == "access_token") {
historyResponse = await http.get(url, headers: {
historyResponse = await http.get(url, headers: {
"authorization": "Bearer $_password",
"Content-Type": "application/json"
});
} else {
historyResponse = await http.get(url, headers: {
"X-HA-Access": "$_password",
"Content-Type": "application/json"
});
}
});
var history = json.decode(historyResponse.body);
if (history is List) {
TheLogger.debug( "[Received] <== ${history.first.length} history recors");
Logger.d( "[Received] <== ${history.first.length} history recors");
return history;
} else {
return [];

View File

@ -19,7 +19,7 @@ class _LogViewPageState extends State<LogViewPage> {
}
_loadLog() async {
_logData = TheLogger.getLog();
_logData = Logger.getLog();
}
@override

View File

@ -1,5 +1,6 @@
import 'dart:convert';
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/rendering.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
@ -11,9 +12,9 @@ import 'package:url_launcher/url_launcher.dart';
import 'package:flutter/services.dart';
import 'package:date_format/date_format.dart';
import 'package:http/http.dart' as http;
import 'package:flutter_colorpicker/material_picker.dart';
import 'package:charts_flutter/flutter.dart' as charts;
import 'package:progress_indicators/progress_indicators.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
part 'entity_class/const.dart';
part 'entity_class/entity.class.dart';
@ -32,6 +33,9 @@ part 'entity_class/media_player_entity.class.dart';
part 'entity_class/lock_entity.class.dart';
part 'entity_class/group_entity.class.dart';
part 'entity_class/fan_entity.class.dart';
part 'entity_class/automation_entity.dart';
part 'entity_class/camera_entity.class.dart';
part 'entity_class/alarm_control_panel.class.dart';
part 'entity_widgets/common/badge.dart';
part 'entity_widgets/model_widgets.dart';
part 'entity_widgets/default_entity_container.dart';
@ -44,6 +48,8 @@ part 'entity_widgets/common/last_updated.dart';
part 'entity_widgets/common/mode_swicth.dart';
part 'entity_widgets/common/mode_selector.dart';
part 'entity_widgets/common/universal_slider.dart';
part 'entity_widgets/common/flat_service_button.dart';
part 'entity_widgets/common/light_color_picker.dart';
part 'entity_widgets/entity_colors.class.dart';
part 'entity_widgets/entity_page_container.dart';
part 'entity_widgets/history_chart/entity_history.dart';
@ -60,14 +66,16 @@ part 'entity_widgets/state/simple_state.dart';
part 'entity_widgets/state/climate_state.dart';
part 'entity_widgets/state/cover_state.dart';
part 'entity_widgets/state/date_time_state.dart';
part 'entity_widgets/state/button_state.dart';
part 'entity_widgets/state/lock_state.dart';
part 'entity_widgets/controls/climate_controls.dart';
part 'entity_widgets/controls/cover_controls.dart';
part 'entity_widgets/controls/light_controls.dart';
part 'entity_widgets/controls/media_player_widgets.dart';
part 'entity_widgets/controls/fan_controls.dart';
part 'entity_widgets/controls/alarm_control_panel_controls.dart';
part 'entity_widgets/controls/camera_controls.dart';
part 'settings.page.dart';
part 'configuration.page.dart';
part 'home_assistant.class.dart';
part 'log.page.dart';
part 'entity.page.dart';
@ -85,14 +93,15 @@ part 'ui_widgets/card_header_widget.dart';
EventBus eventBus = new EventBus();
const String appName = "HA Client";
const appVersion = "0.3.12";
const appVersion = "0.4.0";
const appBuild = "91";
String homeAssistantWebHost;
void main() {
FlutterError.onError = (errorDetails) {
TheLogger.error( "${errorDetails.exception}");
if (TheLogger.isInDebugMode) {
Logger.e( "${errorDetails.exception}");
if (Logger.isInDebugMode) {
FlutterError.dumpErrorToConsole(errorDetails);
}
};
@ -100,9 +109,9 @@ void main() {
runZoned(() {
runApp(new HAClientApp());
}, onError: (error, stack) {
TheLogger.error("$error");
TheLogger.error("$stack");
if (TheLogger.isInDebugMode) {
Logger.e("$error");
Logger.e("$stack");
if (Logger.isInDebugMode) {
debugPrint("$stack");
}
});
@ -121,6 +130,7 @@ class HAClientApp extends StatelessWidget {
routes: {
"/": (context) => MainPage(title: 'HA Client'),
"/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"),
"/configuration": (context) => ConfigurationPage(title: "Configuration"),
"/log-view": (context) => LogViewPage(title: "Log")
},
);
@ -141,14 +151,12 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
//Map _instanceConfig;
String _webSocketApiEndpoint;
String _password;
String _authType;
//int _uiViewsCount = 0;
String _instanceHost;
StreamSubscription _stateSubscription;
StreamSubscription _settingsSubscription;
StreamSubscription _serviceCallSubscription;
StreamSubscription _showEntityPageSubscription;
StreamSubscription _refreshDataSubscription;
StreamSubscription _showErrorSubscription;
bool _settingsLoaded = false;
bool _accountMenuExpanded = false;
@ -160,11 +168,11 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
_settingsLoaded = false;
WidgetsBinding.instance.addObserver(this);
TheLogger.debug("<!!!> Creating new HomeAssistant instance");
Logger.d("<!!!> Creating new HomeAssistant instance");
_homeAssistant = HomeAssistant();
_settingsSubscription = eventBus.on<SettingsChangedEvent>().listen((event) {
TheLogger.debug("Settings change event: reconnect=${event.reconnect}");
Logger.d("Settings change event: reconnect=${event.reconnect}");
if (event.reconnect) {
_homeAssistant.disconnect().then((_){
_initialLoad();
@ -185,7 +193,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
TheLogger.debug("$state");
Logger.d("$state");
if (state == AppLifecycleState.resumed && _settingsLoaded) {
_refreshData();
}
@ -199,8 +207,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
_webSocketApiEndpoint = "${prefs.getString('hassio-protocol')}://$domain:$port/api/websocket";
homeAssistantWebHost = "${prefs.getString('hassio-res-protocol')}://$domain:$port";
_password = prefs.getString('hassio-password');
_authType = prefs.getString('hassio-auth-type');
_useLovelaceUI = prefs.getBool('use-lovelace') ?? false;
_useLovelaceUI = prefs.getBool('use-lovelace') ?? true;
if ((domain == null) || (port == null) || (_password == null) ||
(domain.length == 0) || (port.length == 0) || (_password.length == 0)) {
throw("Check connection settings");
@ -212,7 +219,12 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
_subscribe() {
if (_stateSubscription == null) {
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
setState(() {});
if (event.needToRebuildUI) {
Logger.d("New entity. Need to rebuild UI");
_refreshData();
} else {
setState(() {});
}
});
}
if (_serviceCallSubscription == null) {
@ -230,12 +242,6 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
});
}
if (_refreshDataSubscription == null) {
_refreshDataSubscription = eventBus.on<RefreshDataEvent>().listen((event){
_refreshData();
});
}
if (_showErrorSubscription == null) {
_showErrorSubscription = eventBus.on<ShowErrorEvent>().listen((event){
_showErrorBottomBar(message: event.text, errorCode: event.errorCode);
@ -244,7 +250,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
}
_refreshData() async {
_homeAssistant.updateSettings(_webSocketApiEndpoint, _password, _authType, _useLovelaceUI);
_homeAssistant.updateSettings(_webSocketApiEndpoint, _password, _useLovelaceUI);
_hideBottomBar();
_showInfoBottomBar(progress: true,);
await _homeAssistant.fetch().then((result) {
@ -257,8 +263,8 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
_setErrorState(e) {
if (e is Error) {
TheLogger.error(e.toString());
TheLogger.error("${e.stackTrace}");
Logger.e(e.toString());
Logger.e("${e.stackTrace}");
_showErrorBottomBar(
message: "There was some error",
errorCode: 13
@ -335,6 +341,14 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
]);
} else {
menuItems.addAll([
new ListTile(
leading: Icon(Icons.settings),
title: Text("Configuration"),
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).pushNamed('/configuration');
},
),
new ListTile(
leading: Icon(Icons.insert_drive_file),
title: Text("Log"),
@ -357,10 +371,10 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
GestureDetector(
onTap: () {
Navigator.of(context).pop();
HAUtils.launchURL("http://www.keyboardcrumbs.io/");
HAUtils.launchURL("http://ha-client.homemade.systems/");
},
child: Text(
"www.keyboardcrumbs.io",
"ha-client.homemade.systems",
style: TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline
@ -370,7 +384,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
],
applicationName: appName,
applicationVersion: appVersion,
applicationLegalese: "Keyboard Crumbs",
applicationLegalese: "build $appBuild",
)
]);
}
@ -514,6 +528,26 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
pinned: true,
primary: true,
title: Text(_homeAssistant != null ? _homeAssistant.locationName : ""),
actions: <Widget>[
IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName(
"mdi:dots-vertical"), color: Colors.white,),
onPressed: () {
showMenu(
position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 70.0, 0.0, 0.0),
context: context,
items: [PopupMenuItem<String>(
child: new Text("Reload"),
value: "reload",
)]
).then((String val) {
if (val == "reload") {
_refreshData();
}
});
}
)
],
leading: IconButton(
icon: Icon(Icons.menu),
onPressed: () {
@ -629,7 +663,6 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
if (_settingsSubscription != null) _settingsSubscription.cancel();
if (_serviceCallSubscription != null) _serviceCallSubscription.cancel();
if (_showEntityPageSubscription != null) _showEntityPageSubscription.cancel();
if (_refreshDataSubscription != null) _refreshDataSubscription.cancel();
if (_showErrorSubscription != null) _showErrorSubscription.cancel();
_homeAssistant.disconnect();
super.dispose();

View File

@ -24,7 +24,14 @@ class MaterialDesignIcons {
"cover.opening": "mdi:window-open",
"lock.locked": "mdi:lock",
"lock.unlocked": "mdi:lock-open",
"fan": "mdi:fan"
"fan": "mdi:fan",
"alarm_control_panel.disarmed" : "mdi:bell-outline",
"alarm_control_panel.armed_home" : "mdi:bell-plus",
"alarm_control_panel.armed_away" : "mdi:bell",
"alarm_control_panel.armed_night" : "mdi:bell-sleep",
"alarm_control_panel.armed_custom_bypass" : "mdi:bell",
"alarm_control_panel.triggered" : "mdi:bell-ring",
"alarm_control_panel" : "mdi:bell"
};
static Map _defaultIconsByDeviceClass = {

View File

@ -18,10 +18,8 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
String _newHassioPassword = "";
String _socketProtocol = "wss";
String _newSocketProtocol = "wss";
String _authType = "access_token";
String _newAuthType = "access_token";
bool _useLovelace = false;
bool _newUseLovelace = false;
bool _useLovelace = true;
bool _newUseLovelace = true;
@override
void initState() {
@ -37,11 +35,10 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
_hassioPort = _newHassioPort = prefs.getString("hassio-port") ?? "";
_hassioPassword = _newHassioPassword = prefs.getString("hassio-password") ?? "";
_socketProtocol = _newSocketProtocol = prefs.getString("hassio-protocol") ?? 'wss';
_authType = _newAuthType = prefs.getString("hassio-auth-type") ?? 'access_token';
try {
_useLovelace = _newUseLovelace = prefs.getBool("use-lovelace") ?? false;
_useLovelace = _newUseLovelace = prefs.getBool("use-lovelace") ?? true;
} catch (e) {
_useLovelace = _newUseLovelace = false;
_useLovelace = _newUseLovelace = true;
}
});
}
@ -51,7 +48,6 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
(_newHassioPort != _hassioPort) ||
(_newHassioDomain != _hassioDomain) ||
(_newSocketProtocol != _socketProtocol) ||
(_newAuthType != _authType) ||
(_newUseLovelace != _useLovelace));
}
@ -66,7 +62,6 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
prefs.setString("hassio-password", _newHassioPassword);
prefs.setString("hassio-protocol", _newSocketProtocol);
prefs.setString("hassio-res-protocol", _newSocketProtocol == "wss" ? "https" : "http");
prefs.setString("hassio-auth-type", _newAuthType);
prefs.setBool("use-lovelace", _newUseLovelace);
}
@ -83,13 +78,13 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
icon: Icon(Icons.check),
onPressed: (){
if (_checkConfigChanged()) {
TheLogger.debug("Settings changed. Saving...");
Logger.d("Settings changed. Saving...");
_saveSettings().then((r) {
Navigator.pop(context);
eventBus.fire(SettingsChangedEvent(true));
});
} else {
TheLogger.debug("Settings was not changed");
Logger.d("Settings was not changed");
Navigator.pop(context);
}
}
@ -154,33 +149,9 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
"Try ports 80 and 443 if default is not working and you don't know why.",
style: TextStyle(color: Colors.grey),
),
new Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(
"Login with access token (HA >= 0.78.0)",
softWrap: true,
maxLines: 2,
),
),
Switch(
value: (_newAuthType == "access_token"),
onChanged: (value) {
setState(() {
_newAuthType = value ? "access_token" : "api_password";
});
},
)
],
),
new Text(
"You should use access token for HA >= 0.84.1. Legacy password will not work there.",
style: TextStyle(color: Colors.grey),
),
new TextField(
decoration: InputDecoration(
labelText: _newAuthType == "access_token" ? "Access token" : "API password"
labelText: "Access token"
),
controller: new TextEditingController.fromValue(
new TextEditingValue(

View File

@ -12,6 +12,8 @@ class HACard {
bool showEmpty;
int columnsCount;
List stateFilter;
List states;
String content;
HACard({
this.name,
@ -22,6 +24,8 @@ class HACard {
this.showState: true,
this.stateFilter: const [],
this.showEmpty: true,
this.content,
this.states,
@required this.type
});

View File

@ -1,8 +1,8 @@
part of '../main.dart';
class Sizes {
static const rightWidgetPadding = 14.0;
static const leftWidgetPadding = 8.0;
static const rightWidgetPadding = 16.0;
static const leftWidgetPadding = 16.0;
static const buttonPadding = 4.0;
static const extendedWidgetHeight = 50.0;
static const iconSize = 28.0;

View File

@ -3,14 +3,18 @@ part of '../main.dart';
class CardHeaderWidget extends StatelessWidget {
final String name;
final Widget trailing;
final Widget subtitle;
const CardHeaderWidget({Key key, this.name}) : super(key: key);
const CardHeaderWidget({Key key, this.name, this.trailing, this.subtitle}) : super(key: key);
@override
Widget build(BuildContext context) {
var result;
if ((name != null) && (name.trim().length > 0)) {
result = new ListTile(
trailing: trailing,
subtitle: subtitle,
title: Text("$name",
textAlign: TextAlign.left,
overflow: TextOverflow.ellipsis,

View File

@ -33,16 +33,26 @@ class CardWidget extends StatelessWidget {
return _buildEntityButtonCard(context);
}
case CardType.markdown: {
return _buildMarkdownCard(context);
}
case CardType.alarmPanel: {
return _buildAlarmPanelCard(context);
}
case CardType.horizontalStack: {
if (card.childCards.isNotEmpty) {
List<Widget> children = [];
card.childCards.forEach((card) {
children.add(
Flexible(
fit: FlexFit.tight,
child: card.build(context),
)
);
if (card.getEntitiesToShow().isNotEmpty || card.showEmpty) {
children.add(
Flexible(
fit: FlexFit.tight,
child: card.build(context),
)
);
}
});
return Row(
mainAxisSize: MainAxisSize.max,
@ -107,6 +117,73 @@ class CardWidget extends StatelessWidget {
);
}
Widget _buildMarkdownCard(BuildContext context) {
if (card.content == null) {
return Container(height: 0.0, width: 0.0,);
}
List<Widget> body = [];
body.add(CardHeaderWidget(name: card.name));
body.add(MarkdownBody(data: card.content));
return Card(
child: Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: new Column(mainAxisSize: MainAxisSize.min, children: body),
)
);
}
Widget _buildAlarmPanelCard(BuildContext context) {
if (card.linkedEntityWrapper == null || card.linkedEntityWrapper.entity == null) {
return Container(width: 0, height: 0,);
} else {
List<Widget> body = [];
body.add(CardHeaderWidget(
name: card.name ?? "",
subtitle: Text("${card.linkedEntityWrapper.entity.displayState}",
style: TextStyle(
color: Colors.grey
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
EntityIcon(
iconSize: 50.0,
),
Container(
width: 26.0,
child: IconButton(
padding: EdgeInsets.all(0.0),
alignment: Alignment.centerRight,
icon: Icon(MaterialDesignIcons.createIconDataFromIconName(
"mdi:dots-vertical")),
onPressed: () => eventBus.fire(new ShowEntityPageEvent(card.linkedEntityWrapper.entity))
)
)
]
),
));
body.add(
AlarmControlPanelControlsWidget(
extended: true,
states: card.states,
)
);
return Card(
child: EntityModel(
entityWrapper: card.linkedEntityWrapper,
handleTap: null,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: body
)
)
);
}
}
Widget _buildGlanceCard(BuildContext context) {
List<EntityWrapper> entitiesToShow = card.getEntitiesToShow();
if (entitiesToShow.isEmpty && !card.showEmpty) {

View File

@ -17,29 +17,17 @@ class ViewWidget extends StatefulWidget {
class ViewWidgetState extends State<ViewWidget> {
StreamSubscription _refreshDataSubscription;
Completer _refreshCompleter;
@override
void initState() {
super.initState();
_refreshDataSubscription = eventBus.on<RefreshDataFinishedEvent>().listen((event) {
if ((_refreshCompleter != null) && (!_refreshCompleter.isCompleted)) {
_refreshCompleter.complete();
}
});
}
@override
Widget build(BuildContext context) {
return RefreshIndicator(
color: Colors.amber,
child: ListView(
padding: EdgeInsets.all(0.0),
physics: const AlwaysScrollableScrollPhysics(),
children: _buildChildren(context),
),
onRefresh: () => _refreshData(),
return ListView(
padding: EdgeInsets.all(0.0),
//physics: const AlwaysScrollableScrollPhysics(),
children: _buildChildren(context),
);
}
@ -57,12 +45,22 @@ class ViewWidgetState extends State<ViewWidget> {
);
}
List<Widget> cards = [];
widget.view.cards.forEach((HACard card){
result.add(
card.build(context)
cards.add(
ConstrainedBox(
constraints: BoxConstraints(maxWidth: 500),
child: card.build(context),
)
);
});
result.add(
Column (
children: cards,
)
);
return result;
}
@ -76,19 +74,8 @@ class ViewWidgetState extends State<ViewWidget> {
return result;
}
Future _refreshData() {
if ((_refreshCompleter != null) && (!_refreshCompleter.isCompleted)) {
TheLogger.debug("Previous data refresh is still in progress");
} else {
_refreshCompleter = Completer();
eventBus.fire(RefreshDataEvent());
}
return _refreshCompleter.future;
}
@override
void dispose() {
_refreshDataSubscription.cancel();
super.dispose();
}

View File

@ -1,6 +1,6 @@
part of 'main.dart';
class TheLogger {
class Logger {
static List<String> _log = [];
@ -20,15 +20,15 @@ class TheLogger {
return inDebugMode;
}
static void error(String message) {
static void e(String message) {
_writeToLog("Error", message);
}
static void warning(String message) {
static void w(String message) {
_writeToLog("Warning", message);
}
static void debug(String message) {
static void d(String message) {
_writeToLog("Debug", message);
}
@ -50,7 +50,7 @@ class HAUtils {
if (await canLaunch(url)) {
await launch(url);
} else {
TheLogger.error( "Could not launch $url");
Logger.e( "Could not launch $url");
}
}
}
@ -58,8 +58,13 @@ class HAUtils {
class StateChangedEvent {
String entityId;
String newState;
bool needToRebuildUI;
StateChangedEvent(this.entityId, this.newState);
StateChangedEvent({
this.entityId,
this.newState,
this.needToRebuildUI: false
});
}
class SettingsChangedEvent {
@ -68,10 +73,6 @@ class SettingsChangedEvent {
SettingsChangedEvent(this.reconnect);
}
class RefreshDataEvent {
RefreshDataEvent();
}
class RefreshDataFinishedEvent {
RefreshDataFinishedEvent();
}

View File

@ -7,7 +7,7 @@ packages:
name: archive
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.4"
version: "2.0.8"
args:
dependency: transitive
description:
@ -35,7 +35,7 @@ packages:
name: cached_network_image
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.1"
version: "0.6.0-alpha.2"
charcode:
dependency: transitive
description:
@ -70,7 +70,7 @@ packages:
name: convert
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
version: "2.1.1"
crypto:
dependency: transitive
description:
@ -83,7 +83,7 @@ packages:
description:
path: "."
ref: HEAD
resolved-ref: e26916e095244a7e5ea61315b030d298d127ed26
resolved-ref: a7ed88a4793e094a4d5d5c2d88a89e55510accde
url: "https://github.com/MarkOSullivan94/dart_config.git"
source: git
version: "0.5.0"
@ -93,7 +93,7 @@ packages:
name: date_format
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.5"
version: "1.0.6"
event_bus:
dependency: "direct main"
description:
@ -112,14 +112,7 @@ packages:
name: flutter_cache_manager
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0+1"
flutter_colorpicker:
dependency: "direct main"
description:
name: flutter_colorpicker
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.0"
version: "0.3.0-beta.2+1"
flutter_launcher_icons:
dependency: "direct dev"
description:
@ -127,6 +120,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.7.0"
flutter_markdown:
dependency: "direct main"
description:
name: flutter_markdown
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0"
flutter_test:
dependency: "direct dev"
description: flutter
@ -138,7 +138,7 @@ packages:
name: http
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.0"
version: "0.12.0+1"
http_parser:
dependency: transitive
description:
@ -152,7 +152,7 @@ packages:
name: image
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.5"
version: "2.0.7"
intl:
dependency: transitive
description:
@ -167,6 +167,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.11.3+2"
markdown:
dependency: transitive
description:
name: markdown
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
matcher:
dependency: transitive
description:
@ -201,7 +208,7 @@ packages:
name: petitparser
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
version: "2.1.1"
progress_indicators:
dependency: "direct main"
description:
@ -222,7 +229,7 @@ packages:
name: shared_preferences
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.3"
version: "0.5.1+1"
sky_engine:
dependency: transitive
description: flutter
@ -235,6 +242,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.1"
sqflite:
dependency: transitive
description:
name: sqflite
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.2+1"
stack_trace:
dependency: transitive
description:
@ -262,7 +276,7 @@ packages:
name: synchronized
url: "https://pub.dartlang.org"
source: hosted
version: "1.5.3"
version: "1.5.3+2"
term_glyph:
dependency: transitive
description:
@ -290,7 +304,7 @@ packages:
name: url_launcher
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.2"
version: "5.0.1"
uuid:
dependency: transitive
description:
@ -318,7 +332,7 @@ packages:
name: xml
url: "https://pub.dartlang.org"
source: hosted
version: "3.2.3"
version: "3.3.1"
yaml:
dependency: transitive
description:

View File

@ -1,7 +1,7 @@
name: hass_client
description: Home Assistant Android Client
version: 0.3.12+79
version: 0.4.0+91
environment:
sdk: ">=2.0.0-dev.68.0 <3.0.0"
@ -16,12 +16,8 @@ dependencies:
cached_network_image: any
url_launcher: any
date_format: any
flutter_colorpicker: any
charts_flutter: any
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
#cupertino_icons: ^0.1.2
flutter_markdown: any
dev_dependencies:
flutter_test: