Compare commits

...

105 Commits
0.2.2 ... 0.3.4

Author SHA1 Message Date
b2da9fc04d Fix target temp history 2018-11-03 23:10:25 +02:00
f281fab744 Version 0.3.4 2018-11-03 22:54:36 +02:00
3b99f4feeb Resolves #120 2018-11-03 22:50:21 +02:00
efab8b60b1 WIP #120 null values handling 2018-11-03 21:56:06 +02:00
0e96406573 WIP #120 show all states for climate 2018-11-03 19:54:26 +02:00
ed8757c08d Version code 2018-10-31 01:39:53 +02:00
813770329c WIP #120 render only needed states 2018-10-31 01:37:36 +02:00
1853bd466e WIP #120 combined history state 2018-10-31 01:02:53 +02:00
07258477b3 Unsupported cards improvements 2018-10-30 22:53:49 +02:00
a3adb72cf8 Unsupported lovelace cards showing entities 2018-10-30 22:51:45 +02:00
e25162f7b5 Version code 2018-10-29 23:54:52 +02:00
d30c9d574b WIP #120 Remove custom renderers for dots 2018-10-29 23:30:11 +02:00
efa5a1958c WIP #120 Simple state chart improvements 2018-10-29 23:06:36 +02:00
37f20fae5a Version code change 2018-10-29 00:59:44 +02:00
91db34badb WIP #120 History chart based on attributes 2018-10-29 00:58:52 +02:00
c20200b609 WIP #120 Random color for states 2018-10-28 21:02:38 +02:00
fcd4ac7292 WIP #120 Numeric state charts 2018-10-28 20:01:01 +02:00
e16338c3f2 WIP #120 History widget improvements 2018-10-28 18:07:52 +02:00
6e038b0685 WIP #120 Convert history time to local 2018-10-28 15:25:12 +02:00
052cd3894e WIP #120 Simplest on/off state history chart 2018-10-28 14:56:23 +02:00
809c7d6355 Card separation by type 2018-10-27 17:28:47 +03:00
9edfec7dff Code structure 2018-10-27 14:27:41 +03:00
df56f6ceda version code change 2018-10-27 01:27:12 +03:00
5e834b0645 Logger improvements 2018-10-27 01:24:23 +03:00
8fb0d61a84 Resolves #122 2018-10-27 00:54:05 +03:00
54979b583b version change for internal testing 2018-10-25 00:58:03 +03:00
4e955e98d8 Still #154 default view 2018-10-25 00:54:20 +03:00
88cfcb4382 Resolves #153 hidden entities 2018-10-25 00:13:50 +03:00
5338e45ddc Resolves #154 UI building refactoring 2018-10-25 00:08:26 +03:00
24d071e2f8 WIP #154 UI building refactoring 2018-10-24 23:53:10 +03:00
988cd4a72f Version 0.3.3 2018-10-21 19:19:55 +03:00
d1ea916781 Fix assumed state switch 2018-10-21 19:18:33 +03:00
ce9f25b86c Light color button 2018-10-21 19:12:37 +03:00
f29762c931 Fix hidden group issue 2018-10-21 18:52:29 +03:00
30e4496ef1 Resolves #148 assumed_state support 2018-10-21 17:13:11 +03:00
7f9dc5dd3a Set Light britness to 0 if light is turned off 2018-10-21 16:18:27 +03:00
0f6babc243 Resolves #151 Group visibility support 2018-10-21 16:11:47 +03:00
6a43e04b31 Just small method rename 2018-10-21 15:26:14 +03:00
36fa5a50c4 Remove cancelling null subscription 2018-10-21 14:48:25 +03:00
9ad6d92ccd View entities in entityCollection. Child entities in parse 2018-10-21 14:43:52 +03:00
fafa8f43f4 Minor light fixes 2018-10-21 13:55:18 +03:00
9b490d33d5 Reverting views refactoring 2018-10-21 02:39:51 +03:00
33f9a1075e Remove ViewWrapper widget 2018-10-21 01:09:07 +03:00
b83006e2c3 View as widget refactoring 2018-10-21 00:30:58 +03:00
ba09c36bd2 Resloves #133 Light support 2018-10-18 23:47:55 +03:00
c71ee568b0 Merge pull request #152 from estevez-dev/release/0.3.2
Fix empty cards on default_view
2018-10-18 22:03:51 +03:00
75041f5c23 Fix empty cards on default_view 2018-10-18 21:57:10 +03:00
14da471774 Merge pull request #150 from estevez-dev/release/0.3.1
Resolves #136 cover state
2018-10-17 21:34:36 +03:00
369b44f1c8 Merge branch 'master' into release/0.3.1 2018-10-17 21:34:27 +03:00
8284bb6e76 Resolves #136 cover state 2018-10-17 21:21:00 +03:00
9b3b4dfbbc WIP #133 Lights 2018-10-17 02:19:46 +03:00
5ca4424933 Fix dropdown width 2018-10-16 23:30:17 +03:00
a308aa29a4 Add mode switch stateless widget 2018-10-16 23:20:27 +03:00
9e80b0eaaf Add temperature control stateless widget 2018-10-16 22:35:17 +03:00
85379cf491 Resolves #132 2018-10-16 21:10:59 +03:00
758376a891 Version 0.3.0 2018-10-16 17:53:50 +03:00
2ebba364e3 Resolves #76 Covers support 2018-10-16 17:35:13 +03:00
6e604440c0 Resolves #106 Climate support 2018-10-16 15:14:54 +03:00
c23034688e WIP #106 2018-10-15 18:04:16 +03:00
69f45b52cf WIP #106 2018-10-15 00:29:40 +03:00
ffc053fbe6 Full ui structure refactoring. InheritedWidget as entity model 2018-10-15 00:15:09 +03:00
b5f9ecf601 Minor fixes 2018-10-12 18:03:27 +03:00
948d1d4e23 Resolves #106 Climate support 2018-10-11 23:02:05 +03:00
136297c18b Climate default icon. Icon colors fix 2018-10-08 23:30:09 +03:00
164800951d Resolves #129 2018-10-08 23:11:56 +03:00
84d283de2b VIP #120 2018-10-07 23:06:06 +03:00
2fa35d771a Resolves #123 Account details and settings. Get user name from HA 2018-10-07 20:18:14 +03:00
326cd073b9 Async data fetching 2018-10-07 18:27:10 +03:00
e99c3f5742 Fix wrong password issue and infinity reconnects issue 2018-10-07 18:21:55 +03:00
16a9392fa6 Resolves #79 Too many tabs issue 2018-10-07 17:16:24 +03:00
5bf063969b Resolves #128 Enpty settings change issue 2018-10-07 17:07:06 +03:00
c19a0511a6 Version 0.2.5 2018-10-07 15:08:50 +03:00
a4ac40b366 Resolves #107 Show entity attributes 2018-10-07 15:03:51 +03:00
ce69f044fb Resolves #110: Slider improvements 2018-10-07 12:40:45 +03:00
70b6469bd1 Resolves #118 Fix message queue issue 2018-10-07 12:14:48 +03:00
253316fb1f TODOs 2018-10-07 10:41:41 +03:00
ec71200ab0 Resolves #127 Fix entities order in card 2018-10-07 10:36:50 +03:00
bc1f4eab2e Showing error snakbar improvements. Error icon in header 2018-10-07 10:28:28 +03:00
4085006446 Fix save settings issue 2018-10-07 09:55:37 +03:00
b7fb821abe View now a stateful widget to prevent memory leeks 2018-10-07 09:45:04 +03:00
284e7ba451 Resolves #125 UI building refactored 2018-10-07 02:17:14 +03:00
17a3bd8d35 Resolves #126 Connection settings save button 2018-10-06 20:03:20 +03:00
c2b88c8a12 Resolves #124: Connection handling improvements 2018-10-06 16:01:38 +03:00
c975af4c79 Unnecessary dependency removed 2018-10-03 21:50:11 +03:00
debf1b71f1 Remove some debug messages 2018-10-03 21:42:28 +03:00
4725953b32 Add entity widget type. Preparing to make entity build it's own badge 2018-10-03 16:44:11 +03:00
e7ca1209e2 Update app icon 2018-10-03 16:15:09 +03:00
f9afa663f5 Version code change 2018-10-03 15:55:48 +03:00
5068cbbcf4 Menu quick fix 2018-10-03 15:55:11 +03:00
043d3a9905 Changing only version code 2018-10-03 15:26:46 +03:00
77c5f80c13 Fix fetch timeout on app start 2018-10-03 15:25:01 +03:00
e0d35d07dc Version 0.2.4 2018-10-03 14:37:54 +03:00
285447a5b7 Resolves #114 Error going back from settings 2018-10-03 14:36:23 +03:00
ed3e4ba272 COnnection closing improvements 2018-10-03 10:35:40 +03:00
908563063a Fix input_boolean control 2018-10-03 09:50:14 +03:00
7f2611b410 Version 0.2.3 2018-10-03 00:55:50 +03:00
648750655c Resolves #109 No static width for inputs 2018-10-02 23:21:50 +03:00
8a0d5581d9 Resolves #111: Assumed state 2018-10-02 23:10:40 +03:00
98d716109b Resolves #21: Handling socket disconnect by sink done Future 2018-10-02 22:48:47 +03:00
ebb2f2b4e5 Decline all timeouts as variables 2018-10-02 18:05:50 +03:00
d910e4dd43 Add socket ping interval 2018-10-02 17:42:06 +03:00
95d80fbbfc Resolves #58: Message queue 2018-10-02 17:23:19 +03:00
41297150c2 Implement fetch timer with 30 timeout along with connection timer 2018-10-02 16:00:55 +03:00
b14b248f2f Resolves #72 reconnect on message sending 2018-10-02 15:46:24 +03:00
13fc1bff27 Resolves #61: Prevent second connection opening 2018-10-02 14:50:42 +03:00
85 changed files with 4647 additions and 1238 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 711 B

After

Width:  |  Height:  |  Size: 715 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -1,9 +0,0 @@
part of 'main.dart';
class Badge {
String _entityId;
Badge(String groupId) {
_entityId = groupId;
}
}

View File

@ -1,25 +0,0 @@
part of 'main.dart';
class HACard {
String _entityId;
List _entities;
String _friendlyName;
List get entities => _entities;
String get friendlyName => _friendlyName;
HACard(String groupId, String friendlyName) {
_entityId = groupId;
_entities = [];
_friendlyName = friendlyName;
}
void addEntity(String entityId) {
_entities.add(entityId);
}
void addEntities(List entities) {
_entities.addAll(entities);
}
}

View File

@ -1,9 +1,10 @@
part of 'main.dart';
class EntityViewPage extends StatefulWidget {
EntityViewPage({Key key, this.entity}) : super(key: key);
EntityViewPage({Key key, @required this.entity, @required this.homeAssistant }) : super(key: key);
final Entity entity;
final HomeAssistant homeAssistant;
@override
_EntityViewPageState createState() => new _EntityViewPageState();
@ -11,26 +12,33 @@ class EntityViewPage extends StatefulWidget {
class _EntityViewPageState extends State<EntityViewPage> {
String _title;
Entity _entity;
StreamSubscription _stateSubscription;
@override
void initState() {
super.initState();
_entity = widget.entity;
if (_stateSubscription != null) _stateSubscription.cancel();
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
if (event.entityId == _entity.entityId) {
if (event.entityId == widget.entity.entityId) {
setState(() {});
}
});
_prepareData();
_getHistory();
}
_prepareData() async {
_title = _entity.displayName;
void _prepareData() async {
_title = widget.entity.displayName;
}
void _getHistory() {
/* widget.homeAssistant.getHistory(widget.entity.entityId).then((List history) {
if (history != null) {
}
});*/
}
@override
Widget build(BuildContext context) {
return new Scaffold(
@ -44,7 +52,10 @@ class _EntityViewPageState extends State<EntityViewPage> {
),
body: Padding(
padding: EdgeInsets.all(10.0),
child: _entity.buildWidget(context, false)
child: HomeAssistantModel(
homeAssistant: widget.homeAssistant,
child: widget.entity.buildEntityPageWidget(context)
)
),
);
}

View File

@ -1,24 +1,10 @@
part of '../main.dart';
class _ButtonEntityWidgetState extends _EntityWidgetState {
class ButtonEntity extends Entity {
ButtonEntity(Map rawData) : super(rawData);
@override
void sendNewState(newValue) {
eventBus.fire(new ServiceCallEvent(widget.entity.domain, "turn_on", widget.entity.entityId, null));
}
@override
Widget _buildActionWidget(bool inCard, BuildContext context) {
return FlatButton(
onPressed: (() {
sendNewState(null);
}),
child: Text(
"EXECUTE",
textAlign: TextAlign.right,
style:
new TextStyle(fontSize: Entity.STATE_FONT_SIZE, color: Colors.blue),
),
);
Widget _buildStatePart(BuildContext context) {
return ButtonStateWidget();
}
}

View File

@ -0,0 +1,130 @@
part of '../main.dart';
class ClimateEntity extends Entity {
@override
double widgetHeight = 38.0;
@override
EntityHistoryConfig historyConfig = EntityHistoryConfig(
chartType: EntityHistoryWidgetType.numericAttributes,
numericState: false,
numericAttributesToShow: ["current_temperature"]
);
static const SUPPORT_TARGET_TEMPERATURE = 1;
static const SUPPORT_TARGET_TEMPERATURE_HIGH = 2;
static const SUPPORT_TARGET_TEMPERATURE_LOW = 4;
static const SUPPORT_TARGET_HUMIDITY = 8;
static const SUPPORT_TARGET_HUMIDITY_HIGH = 16;
static const SUPPORT_TARGET_HUMIDITY_LOW = 32;
static const SUPPORT_FAN_MODE = 64;
static const SUPPORT_OPERATION_MODE = 128;
static const SUPPORT_HOLD_MODE = 256;
static const SUPPORT_SWING_MODE = 512;
static const SUPPORT_AWAY_MODE = 1024;
static const SUPPORT_AUX_HEAT = 2048;
static const SUPPORT_ON_OFF = 4096;
bool get supportTargetTemperature => ((attributes["supported_features"] &
ClimateEntity.SUPPORT_TARGET_TEMPERATURE) ==
ClimateEntity.SUPPORT_TARGET_TEMPERATURE);
bool get supportTargetTemperatureHigh => ((attributes["supported_features"] &
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH) ==
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH);
bool get supportTargetTemperatureLow => ((attributes["supported_features"] &
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW) ==
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW);
bool get supportTargetHumidity => ((attributes["supported_features"] &
ClimateEntity.SUPPORT_TARGET_HUMIDITY) ==
ClimateEntity.SUPPORT_TARGET_HUMIDITY);
bool get supportTargetHumidityHigh => ((attributes["supported_features"] &
ClimateEntity.SUPPORT_TARGET_HUMIDITY_HIGH) ==
ClimateEntity.SUPPORT_TARGET_HUMIDITY_HIGH);
bool get supportTargetHumidityLow => ((attributes["supported_features"] &
ClimateEntity.SUPPORT_TARGET_HUMIDITY_LOW) ==
ClimateEntity.SUPPORT_TARGET_HUMIDITY_LOW);
bool get supportFanMode =>
((attributes["supported_features"] & ClimateEntity.SUPPORT_FAN_MODE) ==
ClimateEntity.SUPPORT_FAN_MODE);
bool get supportOperationMode => ((attributes["supported_features"] &
ClimateEntity.SUPPORT_OPERATION_MODE) ==
ClimateEntity.SUPPORT_OPERATION_MODE);
bool get supportHoldMode =>
((attributes["supported_features"] & ClimateEntity.SUPPORT_HOLD_MODE) ==
ClimateEntity.SUPPORT_HOLD_MODE);
bool get supportSwingMode =>
((attributes["supported_features"] & ClimateEntity.SUPPORT_SWING_MODE) ==
ClimateEntity.SUPPORT_SWING_MODE);
bool get supportAwayMode =>
((attributes["supported_features"] & ClimateEntity.SUPPORT_AWAY_MODE) ==
ClimateEntity.SUPPORT_AWAY_MODE);
bool get supportAuxHeat =>
((attributes["supported_features"] & ClimateEntity.SUPPORT_AUX_HEAT) ==
ClimateEntity.SUPPORT_AUX_HEAT);
bool get supportOnOff =>
((attributes["supported_features"] & ClimateEntity.SUPPORT_ON_OFF) ==
ClimateEntity.SUPPORT_ON_OFF);
List<String> get operationList => attributes["operation_list"] != null
? (attributes["operation_list"] as List).cast<String>()
: null;
List<String> get fanList => attributes["fan_list"] != null
? (attributes["fan_list"] as List).cast<String>()
: null;
List<String> get swingList => attributes["swing_list"] != null
? (attributes["swing_list"] as List).cast<String>()
: null;
double get temperature => _getDoubleAttributeValue('temperature');
double get targetHigh => _getDoubleAttributeValue('target_temp_high');
double get targetLow => _getDoubleAttributeValue('target_temp_low');
double get maxTemp => _getDoubleAttributeValue('max_temp') ?? 100.0;
double get minTemp => _getDoubleAttributeValue('min_temp') ?? -100.0;
double get targetHumidity => _getDoubleAttributeValue('humidity');
double get maxHumidity => _getDoubleAttributeValue('max_humidity');
double get minHumidity => _getDoubleAttributeValue('min_humidity');
String get operationMode => attributes['operation_mode'];
String get fanMode => attributes['fan_mode'];
String get swingMode => attributes['swing_mode'];
bool get awayMode => attributes['away_mode'] == "on";
bool get isOff => state == "off";
bool get auxHeat => attributes['aux_heat'] == "on";
ClimateEntity(Map rawData) : super(rawData);
@override
void update(Map rawData) {
super.update(rawData);
if (supportTargetTemperature) {
historyConfig.numericAttributesToShow.add("temperature");
}
if (supportTargetTemperatureHigh) {
historyConfig.numericAttributesToShow.add("target_temp_high");
}
if (supportTargetTemperatureLow) {
historyConfig.numericAttributesToShow.add("target_temp_low");
}
}
@override
Widget _buildStatePart(BuildContext context) {
return ClimateStateWidget();
}
@override
Widget _buildAdditionalControlsForPage(BuildContext context) {
return ClimateControlWidget();
}
@override
double _getDoubleAttributeValue(String attributeName) {
var temp1 = attributes["$attributeName"];
if (temp1 is int) {
return temp1.toDouble();
} else if (temp1 is double) {
return temp1;
} else {
return null;
}
}
}

View File

@ -0,0 +1,62 @@
part of '../main.dart';
class CoverEntity extends Entity {
@override
double widgetHeight = 38.0;
static const SUPPORT_OPEN = 1;
static const SUPPORT_CLOSE = 2;
static const SUPPORT_SET_POSITION = 4;
static const SUPPORT_STOP = 8;
static const SUPPORT_OPEN_TILT = 16;
static const SUPPORT_CLOSE_TILT = 32;
static const SUPPORT_STOP_TILT = 64;
static const SUPPORT_SET_TILT_POSITION = 128;
bool get supportOpen => ((attributes["supported_features"] &
CoverEntity.SUPPORT_OPEN) ==
CoverEntity.SUPPORT_OPEN);
bool get supportClose => ((attributes["supported_features"] &
CoverEntity.SUPPORT_CLOSE) ==
CoverEntity.SUPPORT_CLOSE);
bool get supportSetPosition => ((attributes["supported_features"] &
CoverEntity.SUPPORT_SET_POSITION) ==
CoverEntity.SUPPORT_SET_POSITION);
bool get supportStop => ((attributes["supported_features"] &
CoverEntity.SUPPORT_STOP) ==
CoverEntity.SUPPORT_STOP);
bool get supportOpenTilt => ((attributes["supported_features"] &
CoverEntity.SUPPORT_OPEN_TILT) ==
CoverEntity.SUPPORT_OPEN_TILT);
bool get supportCloseTilt => ((attributes["supported_features"] &
CoverEntity.SUPPORT_CLOSE_TILT) ==
CoverEntity.SUPPORT_CLOSE_TILT);
bool get supportStopTilt => ((attributes["supported_features"] &
CoverEntity.SUPPORT_STOP_TILT) ==
CoverEntity.SUPPORT_STOP_TILT);
bool get supportSetTiltPosition => ((attributes["supported_features"] &
CoverEntity.SUPPORT_SET_TILT_POSITION) ==
CoverEntity.SUPPORT_SET_TILT_POSITION);
double get currentPosition => _getDoubleAttributeValue('current_position');
double get currentTiltPosition => _getDoubleAttributeValue('current_tilt_position');
bool get canBeOpened => ((state != "opening") && (state != "open"));
bool get canBeClosed => ((state != "closing") && (state != "closed"));
bool get canTiltBeOpened => currentPosition < 100;
bool get canTiltBeClosed => currentPosition > 0;
CoverEntity(Map rawData) : super(rawData);
@override
Widget _buildStatePart(BuildContext context) {
return CoverStateWidget();
}
@override
Widget _buildAdditionalControlsForPage(BuildContext context) {
return CoverControlWidget();
}
}

View File

@ -0,0 +1,42 @@
part of '../main.dart';
class DateTimeEntity extends Entity {
bool get hasDate => attributes["has_date"] ?? false;
bool get hasTime => attributes["has_time"] ?? false;
int get year => attributes["year"] ?? 1970;
int get month => attributes["month"] ?? 1;
int get day => attributes["day"] ?? 1;
int get hour => attributes["hour"] ?? 0;
int get minute => attributes["minute"] ?? 0;
int get second => attributes["second"] ?? 0;
String get formattedState => _getFormattedState();
DateTime get dateTimeState => _getDateTimeState();
DateTimeEntity(Map rawData) : super(rawData);
@override
Widget _buildStatePart(BuildContext context) {
return DateTimeStateWidget();
}
DateTime _getDateTimeState() {
return DateTime(
this.year, this.month, this.day, this.hour, this.minute, this.second);
}
String _getFormattedState() {
String formattedState = "";
if (this.hasDate) {
formattedState += formatDate(dateTimeState, [M, ' ', d, ', ', yyyy]);
}
if (this.hasTime) {
formattedState += " " + formatDate(dateTimeState, [HH, ':', nn]);
}
return formattedState;
}
void setNewState(newValue) {
eventBus
.fire(new ServiceCallEvent(domain, "set_datetime", entityId, newValue));
}
}

View File

@ -1,92 +0,0 @@
part of '../main.dart';
class _DateTimeEntityWidgetState extends _EntityWidgetState {
bool get hasDate => widget.entity._attributes["has_date"] ?? false;
bool get hasTime => widget.entity._attributes["has_time"] ?? false;
int get year => widget.entity._attributes["year"] ?? 1970;
int get month => widget.entity._attributes["month"] ?? 1;
int get day => widget.entity._attributes["day"] ?? 1;
int get hour => widget.entity._attributes["hour"] ?? 0;
int get minute => widget.entity._attributes["minute"] ?? 0;
int get second => widget.entity._attributes["second"] ?? 0;
String get formattedState => _getFormattedState();
DateTime get dateTimeState => _getDateTimeState();
DateTime _getDateTimeState() {
return DateTime(this.year, this.month, this.day, this.hour, this.minute, this.second);
}
String _getFormattedState() {
String formattedState = "";
if (this.hasDate) {
formattedState += formatDate(dateTimeState, [M, ' ', d, ', ', yyyy]);
}
if (this.hasTime) {
formattedState += " "+formatDate(dateTimeState, [HH, ':', nn]);
}
return formattedState;
}
@override
void sendNewState(newValue) {
eventBus.fire(new ServiceCallEvent(widget.entity.domain, "set_datetime", widget.entity.entityId,
newValue));
}
@override
Widget _buildActionWidget(bool inCard, BuildContext context) {
return Padding(
padding:
EdgeInsets.fromLTRB(0.0, 0.0, Entity.RIGHT_WIDGET_PADDING, 0.0),
child: GestureDetector(
child: Text(
"$formattedState",
textAlign: TextAlign.right,
style: new TextStyle(
fontSize: Entity.STATE_FONT_SIZE,
)),
onTap: () => _handleStateTap(context),
)
);
}
void _handleStateTap(BuildContext context) {
if (hasDate) {
_showDatePicker(context).then((date) {
if (date != null) {
if (hasTime) {
_showTimePicker(context).then((time){
sendNewState({"date": "${formatDate(date, [yyyy, '-', mm, '-', dd])}", "time": "${formatDate(DateTime(1970, 1, 1, time.hour, time.minute), [HH, ':', nn])}"});
});
} else {
sendNewState({"date": "${formatDate(date, [yyyy, '-', mm, '-', dd])}"});
}
}
});
} else if (hasTime) {
_showTimePicker(context).then((time){
if (time != null) {
sendNewState({"time": "${formatDate(DateTime(1970, 1, 1, time.hour, time.minute), [HH, ':', nn])}"});
}
});
} else {
TheLogger.log("Warning", "${widget.entity.entityId} has no date and no time");
}
}
Future _showDatePicker(BuildContext context) {
return showDatePicker(
context: context,
initialDate: dateTimeState,
firstDate: DateTime(1970),
lastDate: DateTime(2037) //Unix timestamp will finish at Jan 19, 2038
);
}
Future _showTimePicker(BuildContext context) {
return showTimePicker(
context: context,
initialTime: TimeOfDay.fromDateTime(dateTimeState)
);
}
}

View File

@ -1,67 +1,162 @@
part of '../main.dart';
class Entity {
static const STATE_ICONS_COLORS = {
"on": Colors.amber,
"off": Color.fromRGBO(68, 115, 158, 1.0),
"unavailable": Colors.black12,
"unknown": Colors.black12,
"playing": Colors.amber
};
static const RIGHT_WIDGET_PADDING = 14.0;
static const LEFT_WIDGET_PADDING = 8.0;
static const EXTENDED_WIDGET_HEIGHT = 50.0;
static const WIDGET_HEIGHT = 34.0;
static const ICON_SIZE = 28.0;
static const STATE_FONT_SIZE = 16.0;
static const NAME_FONT_SIZE = 16.0;
static const SMALL_FONT_SIZE = 14.0;
static const INPUT_WIDTH = 160.0;
Map _attributes;
String _domain;
String _entityId;
String _state;
static const badgeColors = {
"default": Color.fromRGBO(223, 76, 30, 1.0),
"binary_sensor": Color.fromRGBO(3, 155, 229, 1.0)
};
static List badgeDomains = [
"alarm_control_panel",
"binary_sensor",
"device_tracker",
"updater",
"sun",
"timer",
"sensor"
];
static const rightWidgetPadding = 14.0;
static const leftWidgetPadding = 8.0;
static const extendedWidgetHeight = 50.0;
static const iconSize = 28.0;
static const stateFontSize = 16.0;
static const nameFontSize = 16.0;
static const smallFontSize = 14.0;
static const largeFontSize = 24.0;
static const inputWidth = 160.0;
static const rowPadding = 10.0;
double widgetHeight = 34.0;
Map attributes;
String domain;
String entityId;
String state;
String assumedState;
DateTime _lastUpdated;
String get displayName =>
_attributes["friendly_name"] ?? (_attributes["name"] ?? "_");
String get domain => _domain;
String get entityId => _entityId;
String get state => _state;
set state(value) => _state = value;
List<Entity> childEntities = [];
List<String> attributesToShow = ["all"];
EntityHistoryConfig historyConfig = EntityHistoryConfig(
chartType: EntityHistoryWidgetType.simple
);
String get deviceClass => _attributes["device_class"] ?? null;
String get displayName =>
attributes["friendly_name"] ?? (attributes["name"] ?? "_");
String get deviceClass => attributes["device_class"] ?? null;
bool get isView =>
(_domain == "group") &&
(_attributes != null ? _attributes["view"] ?? false : false);
bool get isGroup => _domain == "group";
String get icon => _attributes["icon"] ?? "";
(domain == "group") &&
(attributes != null ? attributes["view"] ?? false : false);
bool get isGroup => domain == "group";
bool get isBadge => Entity.badgeDomains.contains(domain);
String get icon => attributes["icon"] ?? "";
bool get isOn => state == "on";
String get entityPicture => _attributes["entity_picture"];
String get unitOfMeasurement => _attributes["unit_of_measurement"] ?? "";
List get childEntities => _attributes["entity_id"] ?? [];
String get entityPicture => attributes["entity_picture"];
String get unitOfMeasurement => attributes["unit_of_measurement"] ?? "";
List get childEntityIds => attributes["entity_id"] ?? [];
String get lastUpdated => _getLastUpdatedFormatted();
bool get isHidden => attributes["hidden"] ?? false;
double get doubleState => double.tryParse(state) ?? 0.0;
Entity(Map rawData) {
update(rawData);
}
void update(Map rawData) {
_attributes = rawData["attributes"] ?? {};
_domain = rawData["entity_id"].split(".")[0];
_entityId = rawData["entity_id"];
_state = rawData["state"];
attributes = rawData["attributes"] ?? {};
domain = rawData["entity_id"].split(".")[0];
entityId = rawData["entity_id"];
state = rawData["state"];
assumedState = state;
_lastUpdated = DateTime.tryParse(rawData["last_updated"]);
}
EntityWidget buildWidget(BuildContext context, bool inCard) {
return EntityWidget(
double _getDoubleAttributeValue(String attributeName) {
var temp1 = attributes["$attributeName"];
if (temp1 is int) {
return temp1.toDouble();
} else if (temp1 is double) {
return temp1;
} else {
return double.tryParse("$temp1");
}
}
int _getIntAttributeValue(String attributeName) {
var temp1 = attributes["$attributeName"];
if (temp1 is int) {
return temp1;
} else if (temp1 is double) {
return temp1.round();
} else {
return int.tryParse("$temp1");
}
}
Widget buildDefaultWidget(BuildContext context) {
return EntityModel(
entity: this,
inCard: inCard,
child: DefaultEntityContainer(
state: _buildStatePart(context),
height: widgetHeight,
),
handleTap: true,
);
}
Widget _buildStatePart(BuildContext context) {
return SimpleEntityState();
}
Widget _buildStatePartForPage(BuildContext context) {
return _buildStatePart(context);
}
Widget _buildAdditionalControlsForPage(BuildContext context) {
return Container(
width: 0.0,
height: 0.0,
);
}
Widget buildEntityPageWidget(BuildContext context) {
return EntityModel(
entity: this,
child: EntityPageContainer(children: <Widget>[
DefaultEntityContainer(state: _buildStatePartForPage(context), height: widgetHeight),
LastUpdatedWidget(),
Divider(),
buildHistoryWidget(),
_buildAdditionalControlsForPage(context),
EntityAttributesList()
]),
handleTap: false,
);
}
Widget buildHistoryWidget() {
return EntityHistoryWidget(
config: historyConfig,
);
}
Widget buildBadgeWidget(BuildContext context) {
return EntityModel(
entity: this,
child: BadgeWidget(),
handleTap: true,
);
}
String getAttribute(String attributeName) {
if (attributes != null) {
return attributes["$attributeName"];
}
return null;
}
String _getLastUpdatedFormatted() {
if (_lastUpdated == null) {
return "-";
@ -90,151 +185,4 @@ class Entity {
return "$v $text";
}
}
}
class EntityWidget extends StatefulWidget {
EntityWidget({Key key, this.entity, this.inCard}) : super(key: key);
final Entity entity;
final bool inCard;
@override
_EntityWidgetState createState() {
switch (entity.domain) {
case "automation":
case "input_boolean ":
case "switch":
case "light": {
return _SwitchEntityWidgetState();
}
case "script":
case "scene": {
return _ButtonEntityWidgetState();
}
case "input_datetime": {
return _DateTimeEntityWidgetState();
}
case "input_select": {
return _SelectEntityWidgetState();
}
case "input_number": {
return _SliderEntityWidgetState();
}
case "input_text": {
return _TextEntityWidgetState();
}
default: {
return _EntityWidgetState();
}
}
}
}
class _EntityWidgetState extends State<EntityWidget> {
@override
Widget build(BuildContext context) {
if (widget.inCard) {
return _buildMainWidget(context);
} else {
return ListView(
children: <Widget>[
_buildMainWidget(context),
_buildLastUpdatedWidget()
],
);
}
}
Widget _buildMainWidget(BuildContext context) {
return SizedBox(
height: Entity.WIDGET_HEIGHT,
child: Row(
children: <Widget>[
GestureDetector(
child: _buildIconWidget(),
onTap: widget.inCard ? openEntityPage : null,
),
Expanded(
child: GestureDetector(
child: _buildNameWidget(),
onTap: widget.inCard ? openEntityPage : null,
),
),
_buildActionWidget(widget.inCard, context)
],
),
);
}
void openEntityPage() {
eventBus.fire(new ShowEntityPageEvent(widget.entity));
}
void sendNewState(newState) {
return;
}
Widget buildAdditionalWidget() {
return _buildLastUpdatedWidget();
}
Widget _buildIconWidget() {
return Padding(
padding: EdgeInsets.fromLTRB(Entity.LEFT_WIDGET_PADDING, 0.0, 12.0, 0.0),
child: MaterialDesignIcons.createIconWidgetFromEntityData(
widget.entity,
Entity.ICON_SIZE,
Entity.STATE_ICONS_COLORS[widget.entity.state] ?? Colors.blueGrey),
);
}
Widget _buildLastUpdatedWidget() {
return Padding(
padding: EdgeInsets.fromLTRB(
Entity.LEFT_WIDGET_PADDING, Entity.SMALL_FONT_SIZE, 0.0, 0.0),
child: Text(
'${widget.entity.lastUpdated}',
textAlign: TextAlign.left,
style:
TextStyle(fontSize: Entity.SMALL_FONT_SIZE, color: Colors.black26),
),
);
}
Widget _buildNameWidget() {
return Padding(
padding: EdgeInsets.only(right: 10.0),
child: Text(
"${widget.entity.displayName}",
overflow: TextOverflow.ellipsis,
softWrap: false,
style: TextStyle(fontSize: Entity.NAME_FONT_SIZE),
),
);
}
Widget _buildActionWidget(bool inCard, BuildContext context) {
return Padding(
padding:
EdgeInsets.fromLTRB(0.0, 0.0, Entity.RIGHT_WIDGET_PADDING, 0.0),
child: GestureDetector(
child: Text(
"${widget.entity.state}${widget.entity.unitOfMeasurement}",
textAlign: TextAlign.right,
style: new TextStyle(
fontSize: Entity.STATE_FONT_SIZE,
)),
onTap: openEntityPage,
)
);
}
}

View File

@ -0,0 +1,81 @@
part of '../main.dart';
class LightEntity extends Entity {
static const SUPPORT_BRIGHTNESS = 1;
static const SUPPORT_COLOR_TEMP = 2;
static const SUPPORT_EFFECT = 4;
static const SUPPORT_FLASH = 8;
static const SUPPORT_COLOR = 16;
static const SUPPORT_TRANSITION = 32;
static const SUPPORT_WHITE_VALUE = 128;
bool get supportBrightness => ((attributes["supported_features"] &
LightEntity.SUPPORT_BRIGHTNESS) ==
LightEntity.SUPPORT_BRIGHTNESS);
bool get supportColorTemp => ((attributes["supported_features"] &
LightEntity.SUPPORT_COLOR_TEMP) ==
LightEntity.SUPPORT_COLOR_TEMP);
bool get supportEffect => ((attributes["supported_features"] &
LightEntity.SUPPORT_EFFECT) ==
LightEntity.SUPPORT_EFFECT);
bool get supportFlash => ((attributes["supported_features"] &
LightEntity.SUPPORT_FLASH) ==
LightEntity.SUPPORT_FLASH);
bool get supportColor => ((attributes["supported_features"] &
LightEntity.SUPPORT_COLOR) ==
LightEntity.SUPPORT_COLOR);
bool get supportTransition => ((attributes["supported_features"] &
LightEntity.SUPPORT_TRANSITION) ==
LightEntity.SUPPORT_TRANSITION);
bool get supportWhiteValue => ((attributes["supported_features"] &
LightEntity.SUPPORT_WHITE_VALUE) ==
LightEntity.SUPPORT_WHITE_VALUE);
int get brightness => _getIntAttributeValue("brightness");
int get colorTemp => _getIntAttributeValue("color_temp");
double get maxMireds => _getDoubleAttributeValue("max_mireds");
double get minMireds => _getDoubleAttributeValue("min_mireds");
Color get color => _getColor();
bool get isAdditionalControls => ((attributes["supported_features"] != null) && (attributes["supported_features"] != 0));
List<String> get effectList => _getEffectList();
LightEntity(Map rawData) : super(rawData);
Color _getColor() {
List rgb = attributes["rgb_color"];
try {
if ((rgb != null) && (rgb.length > 0)) {
return Color.fromARGB(255, rgb[0], rgb[1], rgb[2]);
} else {
return null;
}
} catch (e) {
return null;
}
}
List<String> _getEffectList() {
if (attributes["effect_list"] != null) {
List<String> result = (attributes["effect_list"] as List).cast<String>();
return result;
} else {
return null;
}
}
@override
Widget _buildStatePart(BuildContext context) {
return SwitchStateWidget();
}
@override
Widget _buildAdditionalControlsForPage(BuildContext context) {
if (!isAdditionalControls) {
return Container(height: 0.0, width: 0.0);
} else {
return LightControlsWidget();
}
}
}

View File

@ -0,0 +1,17 @@
part of '../main.dart';
class SunEntity extends Entity {
SunEntity(Map rawData) : super(rawData);
}
class SensorEntity extends Entity {
@override
EntityHistoryConfig historyConfig = EntityHistoryConfig(
chartType: EntityHistoryWidgetType.numericState,
numericState: true
);
SensorEntity(Map rawData) : super(rawData);
}

View File

@ -1,36 +1,14 @@
part of '../main.dart';
class _SelectEntityWidgetState extends _EntityWidgetState {
List<String> _listOptions = [];
class SelectEntity extends Entity {
List<String> get listOptions => attributes["options"] != null
? (attributes["options"] as List).cast<String>()
: [];
SelectEntity(Map rawData) : super(rawData);
@override
void sendNewState(newValue) {
eventBus.fire(new ServiceCallEvent(widget.entity.domain, "select_option", widget.entity.entityId,
{"option": "$newValue"}));
}
@override
Widget _buildActionWidget(bool inCard, BuildContext context) {
_listOptions.clear();
if (widget.entity._attributes["options"] != null) {
widget.entity._attributes["options"].forEach((value){
_listOptions.add(value.toString());
});
}
return Container(
width: Entity.INPUT_WIDTH,
child: DropdownButton<String>(
value: widget.entity.state,
items: this._listOptions.map((String value) {
return new DropdownMenuItem<String>(
value: value,
child: new Text(value),
);
}).toList(),
onChanged: (_) {
sendNewState(_);
},
),
);
Widget _buildStatePart(BuildContext context) {
return SelectStateWidget();
}
}

View File

@ -1,65 +0,0 @@
part of '../main.dart';
class _SliderEntityWidgetState extends _EntityWidgetState {
int _multiplier = 1;
double get minValue => widget.entity._attributes["min"] ?? 0.0;
double get maxValue => widget.entity._attributes["max"] ?? 100.0;
double get valueStep => widget.entity._attributes["step"] ?? 1.0;
double get doubleState => double.tryParse(widget.entity.state) ?? 0.0;
@override
void initState() {
super.initState();
}
@override
void sendNewState(newValue) {
eventBus.fire(new ServiceCallEvent(widget.entity.domain, "set_value", widget.entity.entityId,
{"value": "${newValue.toString()}"}));
}
@override
Widget _buildActionWidget(bool inCard, BuildContext context) {
if (valueStep < 1) {
_multiplier = 10;
} else if (valueStep < 0.1) {
_multiplier = 100;
}
return Container(
width: 200.0,
child: Row(
children: <Widget>[
Expanded(
child: Slider(
min: this.minValue * _multiplier,
max: this.maxValue * _multiplier,
value: (doubleState <= this.maxValue) &&
(doubleState >= this.minValue)
? doubleState * _multiplier
: this.minValue * _multiplier,
onChanged: (value) {
setState(() {
widget.entity.state = (value.roundToDouble() / _multiplier).toString();
});
/*eventBus.fire(new StateChangedEvent(widget.entity.entityId,
(value.roundToDouble() / _multiplier).toString(), true));*/
},
onChangeEnd: (value) {
sendNewState(value.roundToDouble() / _multiplier);
},
),
),
Padding(
padding: EdgeInsets.only(right: Entity.RIGHT_WIDGET_PADDING),
child: Text("${widget.entity.state}${widget.entity.unitOfMeasurement}",
textAlign: TextAlign.right,
style: new TextStyle(
fontSize: Entity.STATE_FONT_SIZE,
)),
)
],
),
);
}
}

View File

@ -0,0 +1,36 @@
part of '../main.dart';
class SliderEntity extends Entity {
SliderEntity(Map rawData) : super(rawData);
double get minValue => _getDoubleAttributeValue("min") ?? 0.0;
double get maxValue =>_getDoubleAttributeValue("max") ?? 100.0;
double get valueStep => _getDoubleAttributeValue("step") ?? 1.0;
@override
Widget _buildStatePart(BuildContext context) {
return Expanded(
//width: 200.0,
child: Row(
children: <Widget>[
SliderStateWidget(
expanded: true,
),
SimpleEntityState(),
],
),
);
}
@override
Widget _buildStatePartForPage(BuildContext context) {
return SimpleEntityState();
}
@override
Widget _buildAdditionalControlsForPage(BuildContext context) {
return SliderStateWidget(
expanded: false,
);
}
}

View File

@ -1,26 +1,10 @@
part of '../main.dart';
class _SwitchEntityWidgetState extends _EntityWidgetState {
class SwitchEntity extends Entity {
SwitchEntity(Map rawData) : super(rawData);
@override
void initState() {
super.initState();
}
@override
void sendNewState(newValue) {
eventBus.fire(new ServiceCallEvent(
widget.entity.domain, (newValue as bool) ? "turn_on" : "turn_off", widget.entity.entityId, null));
}
@override
Widget _buildActionWidget(bool inCard, BuildContext context) {
return Switch(
value: widget.entity.isOn,
onChanged: ((switchState) {
sendNewState(switchState);
widget.entity.state = switchState ? 'on' : 'off';
}),
);
Widget _buildStatePart(BuildContext context) {
return SwitchStateWidget();
}
}

View File

@ -1,83 +1,16 @@
part of '../main.dart';
class _TextEntityWidgetState extends _EntityWidgetState {
String _tmpValue;
FocusNode _focusNode = FocusNode();
bool validValue = false;
class TextEntity extends Entity {
TextEntity(Map rawData) : super(rawData);
int get valueMinLength => widget.entity._attributes["min"] ?? -1;
int get valueMaxLength => widget.entity._attributes["max"] ?? -1;
String get valuePattern => widget.entity._attributes["pattern"] ?? null;
bool get isTextField => widget.entity._attributes["mode"] == "text";
bool get isPasswordField => widget.entity._attributes["mode"] == "password";
int get valueMinLength => attributes["min"] ?? -1;
int get valueMaxLength => attributes["max"] ?? -1;
String get valuePattern => attributes["pattern"] ?? null;
bool get isTextField => attributes["mode"] == "text";
bool get isPasswordField => attributes["mode"] == "password";
@override
void initState() {
super.initState();
_focusNode.addListener(_focusListener);
_tmpValue = widget.entity.state;
Widget _buildStatePart(BuildContext context) {
return TextInputStateWidget();
}
@override
void sendNewState(newValue) {
if (validate(newValue)) {
eventBus.fire(new ServiceCallEvent(widget.entity.domain, "set_value", widget.entity.entityId,
{"value": "$newValue"}));
} else {
setState(() {
_tmpValue = widget.entity.state;
});
}
}
bool validate(newValue) {
if (newValue is String) {
validValue = (newValue.length >= this.valueMinLength) &&
(this.valueMaxLength == -1 ||
(newValue.length <= this.valueMaxLength));
} else {
validValue = true;
}
return validValue;
}
void _focusListener() {
if (!_focusNode.hasFocus && (_tmpValue != widget.entity.state)) {
sendNewState(_tmpValue);
}
}
@override
Widget _buildActionWidget(bool inCard, BuildContext context) {
if (!_focusNode.hasFocus && (_tmpValue != widget.entity.state)) {
_tmpValue = widget.entity.state;
}
if (this.isTextField || this.isPasswordField) {
return Container(
width: Entity.INPUT_WIDTH,
child: TextField(
focusNode: _focusNode,
obscureText: this.isPasswordField,
controller: new TextEditingController.fromValue(
new TextEditingValue(
text: _tmpValue,
selection:
new TextSelection.collapsed(offset: _tmpValue.length))),
onChanged: (value) {
_tmpValue = value;
}),
);
} else {
TheLogger.log("Warning", "Unsupported input mode for ${widget.entity.entityId}");
return super._buildActionWidget(inCard, context);
}
}
@override
void dispose() {
_focusNode.removeListener(_focusListener);
_focusNode.dispose();
super.dispose();
}
}

View File

@ -2,33 +2,80 @@ part of 'main.dart';
class EntityCollection {
Map<String, Entity> _entities;
List<String> viewList;
Map<String, Entity> _allEntities;
//Map<String, Entity> views;
bool get isEmpty => _allEntities.isEmpty;
List<Entity> get viewEntities => _allEntities.values.where((entity) => entity.isView).toList();
EntityCollection() {
_entities = {};
viewList = [];
_allEntities = {};
//views = {};
}
bool get hasDefaultView => _entities["group.default_view"] != null;
bool get hasDefaultView => _allEntities.keys.contains("group.default_view");
void parse(List rawData) {
_entities.clear();
viewList.clear();
_allEntities.clear();
//views.clear();
TheLogger.log("Debug","Parsing ${rawData.length} Home Assistant entities");
TheLogger.debug("Parsing ${rawData.length} Home Assistant entities");
rawData.forEach((rawEntityData) {
Entity newEntity = addFromRaw(rawEntityData);
if (newEntity.isView) {
viewList.add(newEntity.entityId);
addFromRaw(rawEntityData);
});
_allEntities.forEach((entityId, entity){
if ((entity.isGroup) && (entity.childEntityIds != null)) {
entity.childEntities = getAll(entity.childEntityIds);
}
/*if (entity.isView) {
views[entityId] = entity;
}*/
});
}
Entity _createEntityInstance(rawEntityData) {
switch (rawEntityData["entity_id"].split(".")[0]) {
case 'sun': {
return SunEntity(rawEntityData);
}
case 'sensor': {
return SensorEntity(rawEntityData);
}
case "automation":
case "input_boolean":
case "switch": {
return SwitchEntity(rawEntityData);
}
case "light": {
return LightEntity(rawEntityData);
}
case "script":
case "scene": {
return ButtonEntity(rawEntityData);
}
case "input_datetime": {
return DateTimeEntity(rawEntityData);
}
case "input_select": {
return SelectEntity(rawEntityData);
}
case "input_number": {
return SliderEntity(rawEntityData);
}
case "input_text": {
return TextEntity(rawEntityData);
}
case "climate": {
return ClimateEntity(rawEntityData);
}
case "cover": {
return CoverEntity(rawEntityData);
}
default: {
return Entity(rawEntityData);
}
}
}
void updateState(Map rawStateData) {
if (isExist(rawStateData["entity_id"])) {
@ -39,53 +86,64 @@ class EntityCollection {
}
void add(Entity entity) {
_entities[entity.entityId] = entity;
_allEntities[entity.entityId] = entity;
}
Entity addFromRaw(Map rawEntityData) {
Entity entity = _createEntityInstance(rawEntityData);
_entities[entity.entityId] = entity;
_allEntities[entity.entityId] = entity;
return entity;
}
void updateFromRaw(Map rawEntityData) {
//TODO pass entity in this function and call update from it
_entities[rawEntityData["entity_id"]].update(rawEntityData);
get("${rawEntityData["entity_id"]}")?.update(rawEntityData);
}
Entity get(String entityId) {
return _entities[entityId];
return _allEntities[entityId];
}
List<Entity> getAll(List ids) {
List<Entity> result = [];
ids.forEach((id){
Entity en = get(id);
if (en != null) {
result.add(en);
}
});
return result;
}
bool isExist(String entityId) {
return _entities[entityId] != null;
return _allEntities[entityId] != null;
}
Map<String,List<String>> getDefaultViewTopLevelEntities() {
Map<String,List<String>> result = {"userGroups": [], "notGroupedEntities": []};
List<String> entities = [];
_entities.forEach((id, entity){
List<Entity> filterEntitiesForDefaultView() {
List<Entity> result = [];
List<Entity> groups = [];
List<Entity> nonGroupEntities = [];
_allEntities.forEach((id, entity){
if ((id.indexOf("group.") == 0) && (id.indexOf(".all_") == -1) && (!entity.isView)) {
result["userGroups"].add(id);
groups.add(entity);
}
if (!entity.isGroup) {
entities.add(id);
nonGroupEntities.add(entity);
}
});
entities.forEach((entiyId) {
nonGroupEntities.forEach((entity) {
bool foundInGroup = false;
result["userGroups"].forEach((userGroupId) {
if (_entities[userGroupId].childEntities.contains(entiyId)) {
groups.forEach((groupEntity) {
if (groupEntity.childEntityIds.contains(entity.entityId)) {
foundInGroup = true;
}
});
if (!foundInGroup) {
result["notGroupedEntities"].add(entiyId);
result.add(entity);
}
});
result.insertAll(0, groups);
return result;
}
}

View File

@ -0,0 +1,125 @@
part of '../main.dart';
class BadgeWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
double iconSize = 26.0;
Widget badgeIcon;
String onBadgeTextValue;
Color iconColor = Entity.badgeColors[entityModel.entity.domain] ??
Entity.badgeColors["default"];
switch (entityModel.entity.domain) {
case "sun":
{
badgeIcon = entityModel.entity.state == "below_horizon"
? Icon(
MaterialDesignIcons.createIconDataFromIconCode(0xf0dc),
size: iconSize,
)
: Icon(
MaterialDesignIcons.createIconDataFromIconCode(0xf5a8),
size: iconSize,
);
break;
}
case "sensor":
{
onBadgeTextValue = entityModel.entity.unitOfMeasurement;
badgeIcon = Center(
child: Text(
"${entityModel.entity.state}",
overflow: TextOverflow.fade,
softWrap: false,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 17.0),
),
);
break;
}
case "device_tracker":
{
badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData(
entityModel.entity, iconSize, Colors.black);
onBadgeTextValue = entityModel.entity.state;
break;
}
default:
{
badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData(
entityModel.entity, iconSize, Colors.black);
}
}
Widget onBadgeText;
if (onBadgeTextValue == null || onBadgeTextValue.length == 0) {
onBadgeText = Container(width: 0.0, height: 0.0);
} else {
onBadgeText = Container(
padding: EdgeInsets.fromLTRB(6.0, 2.0, 6.0, 2.0),
child: Text("$onBadgeTextValue",
style: TextStyle(fontSize: 12.0, color: Colors.white),
textAlign: TextAlign.center,
softWrap: false,
overflow: TextOverflow.fade),
decoration: new BoxDecoration(
// Circle shape
//shape: BoxShape.circle,
color: iconColor,
borderRadius: BorderRadius.circular(9.0),
));
}
return GestureDetector(
child: Column(
children: <Widget>[
Container(
margin: EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 10.0),
width: 50.0,
height: 50.0,
decoration: new BoxDecoration(
// Circle shape
shape: BoxShape.circle,
color: Colors.white,
// The border you want
border: new Border.all(
width: 2.0,
color: iconColor,
),
),
child: Stack(
overflow: Overflow.visible,
children: <Widget>[
Positioned(
width: 46.0,
height: 46.0,
top: 0.0,
left: 0.0,
child: badgeIcon,
),
Positioned(
//width: 50.0,
bottom: -9.0,
left: -10.0,
right: -10.0,
child: Center(
child: onBadgeText,
))
],
),
),
Container(
width: 60.0,
child: Text(
"${entityModel.entity.displayName}",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 12.0),
softWrap: true,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
),
],
),
onTap: () =>
eventBus.fire(new ShowEntityPageEvent(entityModel.entity)));
}
}

View File

@ -0,0 +1,467 @@
part of '../../main.dart';
class ClimateControlWidget extends StatefulWidget {
ClimateControlWidget({Key key}) : super(key: key);
@override
_ClimateControlWidgetState createState() => _ClimateControlWidgetState();
}
class _ClimateControlWidgetState extends State<ClimateControlWidget> {
bool _showPending = false;
bool _changedHere = false;
Timer _resetTimer;
double _tmpTemperature = 0.0;
double _tmpTargetLow = 0.0;
double _tmpTargetHigh = 0.0;
double _tmpTargetHumidity = 0.0;
String _tmpOperationMode;
String _tmpFanMode;
String _tmpSwingMode;
bool _tmpAwayMode = false;
bool _tmpIsOff = false;
bool _tmpAuxHeat = false;
void _resetVars(ClimateEntity entity) {
_tmpTemperature = entity.temperature;
_tmpTargetHigh = entity.targetHigh;
_tmpTargetLow = entity.targetLow;
_tmpOperationMode = entity.operationMode;
_tmpFanMode = entity.fanMode;
_tmpSwingMode = entity.swingMode;
_tmpAwayMode = entity.awayMode;
_tmpIsOff = entity.isOff;
_tmpAuxHeat = entity.auxHeat;
_tmpTargetHumidity = entity.targetHumidity;
_showPending = false;
_changedHere = false;
}
void _temperatureUp(ClimateEntity entity, double step) {
_tmpTemperature = ((_tmpTemperature + step) <= entity.maxTemp) ? _tmpTemperature + step : entity.maxTemp;
_setTemperature(entity);
}
void _temperatureDown(ClimateEntity entity, double step) {
_tmpTemperature = ((_tmpTemperature - step) >= entity.minTemp) ? _tmpTemperature - step : entity.minTemp;
_setTemperature(entity);
}
void _targetLowUp(ClimateEntity entity, double step) {
_tmpTargetLow = ((_tmpTargetLow + step) <= entity.maxTemp) ? _tmpTargetLow + step : entity.maxTemp;
_setTargetTemp(entity);
}
void _targetLowDown(ClimateEntity entity, double step) {
_tmpTargetLow = ((_tmpTargetLow - step) >= entity.minTemp) ? _tmpTargetLow - step : entity.minTemp;
_setTargetTemp(entity);
}
void _targetHighUp(ClimateEntity entity, double step) {
_tmpTargetHigh = ((_tmpTargetHigh + step) <= entity.maxTemp) ? _tmpTargetHigh + step : entity.maxTemp;
_setTargetTemp(entity);
}
void _targetHighDown(ClimateEntity entity, double step) {
_tmpTargetHigh = ((_tmpTargetHigh - step) >= entity.minTemp) ? _tmpTargetHigh - step : entity.minTemp;
_setTargetTemp(entity);
}
void _setTemperature(ClimateEntity entity) {
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);
});
}
void _setTargetTemp(ClimateEntity entity) {
setState(() {
_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);
});
}
void _setTargetHumidity(ClimateEntity entity, double value) {
setState(() {
_tmpTargetHumidity = value.roundToDouble();
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_humidity", entity.entityId,{"humidity": "$_tmpTargetHumidity"}));
_resetStateTimer(entity);
});
}
void _setOperationMode(ClimateEntity entity, value) {
setState(() {
_tmpOperationMode = value;
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_operation_mode", entity.entityId,{"operation_mode": "$_tmpOperationMode"}));
_resetStateTimer(entity);
});
}
void _setSwingMode(ClimateEntity entity, value) {
setState(() {
_tmpSwingMode = value;
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_swing_mode", entity.entityId,{"swing_mode": "$_tmpSwingMode"}));
_resetStateTimer(entity);
});
}
void _setFanMode(ClimateEntity entity, value) {
setState(() {
_tmpFanMode = value;
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_fan_mode", entity.entityId,{"fan_mode": "$_tmpFanMode"}));
_resetStateTimer(entity);
});
}
void _setAwayMode(ClimateEntity entity, value) {
setState(() {
_tmpAwayMode = value;
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_away_mode", entity.entityId,{"away_mode": "${_tmpAwayMode ? 'on' : 'off'}"}));
_resetStateTimer(entity);
});
}
void _setOnOf(ClimateEntity entity, value) {
setState(() {
_tmpIsOff = !value;
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "${_tmpIsOff ? 'turn_off' : 'turn_on'}", entity.entityId, null));
_resetStateTimer(entity);
});
}
void _setAuxHeat(ClimateEntity entity, value) {
setState(() {
_tmpAuxHeat = value;
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_aux_heat", entity.entityId, {"aux_heat": "$_tmpAuxHeat"}));
_resetStateTimer(entity);
});
}
void _resetStateTimer(ClimateEntity entity) {
if (_resetTimer!=null) {
_resetTimer.cancel();
}
_resetTimer = Timer(Duration(seconds: 3), () {
setState(() {});
_resetVars(entity);
});
}
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final ClimateEntity entity = entityModel.entity;
if (_changedHere) {
_showPending = (_tmpTemperature != entity.temperature);
_changedHere = false;
} else {
_resetTimer?.cancel();
_resetVars(entity);
}
return Padding(
padding: EdgeInsets.fromLTRB(Entity.leftWidgetPadding, Entity.rowPadding, Entity.rightWidgetPadding, 0.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_buildOnOffControl(entity),
_buildTemperatureControls(entity),
_buildTargetTemperatureControls(entity),
_buildHumidityControls(entity),
_buildOperationControl(entity),
_buildFanControl(entity),
_buildSwingControl(entity),
_buildAwayModeControl(entity),
_buildAuxHeatControl(entity)
],
),
);
}
Widget _buildAwayModeControl(ClimateEntity entity) {
if (entity.supportAwayMode) {
return ModeSwitchWidget(
caption: "Away mode",
onChange: (value) => _setAwayMode(entity, value),
value: _tmpAwayMode,
);
} else {
return Container(height: 0.0, width: 0.0,);
}
}
Widget _buildOnOffControl(ClimateEntity entity) {
if (entity.supportOnOff) {
return ModeSwitchWidget(
onChange: (value) => _setOnOf(entity, value),
caption: "On / Off",
value: !_tmpIsOff
);
} else {
return Container(height: 0.0, width: 0.0,);
}
}
Widget _buildAuxHeatControl(ClimateEntity entity) {
if (entity.supportAuxHeat ) {
return ModeSwitchWidget(
caption: "Aux heat",
onChange: (value) => _setAuxHeat(entity, value),
value: _tmpAuxHeat
);
} else {
return Container(height: 0.0, width: 0.0,);
}
}
Widget _buildOperationControl(ClimateEntity entity) {
if (entity.supportOperationMode) {
return ModeSelectorWidget(
onChange: (mode) => _setOperationMode(entity, mode),
options: entity.operationList,
caption: "Operation",
value: _tmpOperationMode,
);
} else {
return Container(height: 0.0, width: 0.0);
}
}
Widget _buildFanControl(ClimateEntity entity) {
if (entity.supportFanMode) {
return ModeSelectorWidget(
options: entity.fanList,
onChange: (mode) => _setFanMode(entity, mode),
caption: "Fan mode",
value: _tmpFanMode,
);
} else {
return Container(height: 0.0, width: 0.0);
}
}
Widget _buildSwingControl(ClimateEntity entity) {
if (entity.supportSwingMode) {
return ModeSelectorWidget(
onChange: (mode) => _setSwingMode(entity, mode),
options: entity.swingList,
value: _tmpSwingMode,
caption: "Swing mode"
);
} else {
return Container(height: 0.0, width: 0.0);
}
}
Widget _buildTemperatureControls(ClimateEntity entity) {
if ((entity.supportTargetTemperature) && (entity.temperature != null)) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text("Target temperature", style: TextStyle(
fontSize: Entity.stateFontSize
)),
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),
)
],
);
} else {
return Container(width: 0.0, height: 0.0,);
}
}
Widget _buildTargetTemperatureControls(ClimateEntity entity) {
List<Widget> controls = [];
if ((entity.supportTargetTemperatureLow) && (entity.targetLow != null)) {
controls.addAll(<Widget>[
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),
),
Expanded(
child: Container(height: 10.0),
)
]);
}
if ((entity.supportTargetTemperatureHigh) && (entity.targetHigh != null)) {
controls.add(
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),
)
);
}
if (controls.isNotEmpty) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text("Target temperature range", style: TextStyle(
fontSize: Entity.stateFontSize
)),
Row(
children: controls,
)
],
);
} else {
return Container(width: 0.0, height: 0.0);
}
}
Widget _buildHumidityControls(ClimateEntity entity) {
List<Widget> result = [];
if (entity.supportTargetHumidity) {
result.addAll(<Widget>[
Text(
"$_tmpTargetHumidity%",
style: TextStyle(fontSize: Entity.largeFontSize),
),
Expanded(
child: Slider(
value: _tmpTargetHumidity,
max: entity.maxHumidity,
min: entity.minHumidity,
onChanged: ((double val) {
setState(() {
_changedHere = true;
_tmpTargetHumidity = val.roundToDouble();
});
}),
onChangeEnd: (double v) => _setTargetHumidity(entity, v),
),
)
]);
}
if (result.isNotEmpty) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: EdgeInsets.fromLTRB(
0.0, Entity.rowPadding, 0.0, Entity.rowPadding),
child: Text("Target humidity", style: TextStyle(
fontSize: Entity.stateFontSize
)),
),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: result,
),
Container(
height: Entity.rowPadding,
)
],
);
} else {
return Container(
width: 0.0,
height: 0.0,
);
}
}
@override
void dispose() {
_resetTimer?.cancel();
super.dispose();
}
}
class TemperatureControlWidget extends StatelessWidget {
final double value;
final double fontSize;
final Color fontColor;
final onSmallInc;
final onLargeInc;
final onSmallDec;
final onLargeDec;
TemperatureControlWidget(
{Key key,
@required this.value,
@required this.onSmallInc,
@required this.onSmallDec,
@required this.onLargeInc,
@required this.onLargeDec,
this.fontSize,
this.fontColor})
: super(key: key);
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(
"$value",
style: TextStyle(
fontSize: fontSize ?? 24.0,
color: fontColor ?? Colors.black
),
),
Column(
children: <Widget>[
IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName(
'mdi:chevron-up')),
iconSize: 30.0,
onPressed: () => onSmallInc(),
),
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(),
)
],
)
],
);
}
}

View File

@ -0,0 +1,201 @@
part of '../../main.dart';
class CoverControlWidget extends StatefulWidget {
CoverControlWidget({Key key}) : super(key: key);
@override
_CoverControlWidgetState createState() => _CoverControlWidgetState();
}
class _CoverControlWidgetState extends State<CoverControlWidget> {
double _tmpPosition = 0.0;
double _tmpTiltPosition = 0.0;
bool _changedHere = false;
void _setNewPosition(CoverEntity entity, double position) {
setState(() {
_tmpPosition = position.roundToDouble();
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_cover_position", entity.entityId,{"position": _tmpPosition.round()}));
});
}
void _setNewTiltPosition(CoverEntity entity, double position) {
setState(() {
_tmpTiltPosition = position.roundToDouble();
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_cover_tilt_position", entity.entityId,{"tilt_position": _tmpTiltPosition.round()}));
});
}
void _resetVars(CoverEntity entity) {
_tmpPosition = entity.currentPosition;
_tmpTiltPosition = entity.currentTiltPosition;
}
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final CoverEntity entity = entityModel.entity;
TheLogger.debug("${entity.state}");
if (_changedHere) {
_changedHere = false;
} else {
_resetVars(entity);
}
return Padding(
padding: EdgeInsets.fromLTRB(Entity.leftWidgetPadding, Entity.rowPadding, Entity.rightWidgetPadding, 0.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_buildPositionControls(entity),
_buildTiltControls(entity)
],
),
);
}
Widget _buildPositionControls(CoverEntity entity) {
if (entity.supportSetPosition) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: EdgeInsets.fromLTRB(
0.0, Entity.rowPadding, 0.0, Entity.rowPadding),
child: Text("Position", style: TextStyle(
fontSize: Entity.stateFontSize
)),
),
Slider(
value: _tmpPosition,
min: 0.0,
max: 100.0,
divisions: 10,
onChanged: (double value) {
setState(() {
_tmpPosition = value.roundToDouble();
_changedHere = true;
});
},
onChangeEnd: (double value) => _setNewPosition(entity, value),
),
Container(height: Entity.rowPadding,)
],
);
} else {
return Container(width: 0.0, height: 0.0);
}
}
Widget _buildTiltControls(CoverEntity entity) {
List<Widget> controls = [];
if (entity.supportCloseTilt || entity.supportOpenTilt || entity.supportStopTilt) {
controls.add(
CoverTiltControlsWidget()
);
}
if (entity.supportSetTiltPosition) {
controls.addAll(<Widget>[
Slider(
value: _tmpTiltPosition,
min: 0.0,
max: 100.0,
divisions: 10,
onChanged: (double value) {
setState(() {
_tmpTiltPosition = value.roundToDouble();
_changedHere = true;
});
},
onChangeEnd: (double value) => _setNewTiltPosition(entity, value),
),
Container(height: Entity.rowPadding,)
]);
}
if (controls.isNotEmpty) {
controls.insert(0, Padding(
padding: EdgeInsets.fromLTRB(
0.0, Entity.rowPadding, 0.0, Entity.rowPadding),
child: Text("Tilt position", style: TextStyle(
fontSize: Entity.stateFontSize
)),
));
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: controls,
);
} else {
return Container(width: 0.0, height: 0.0);
}
}
}
class CoverTiltControlsWidget extends StatelessWidget {
void _open(CoverEntity entity) {
eventBus.fire(new ServiceCallEvent(
entity.domain, "open_cover_tilt", entity.entityId, null));
}
void _close(CoverEntity entity) {
eventBus.fire(new ServiceCallEvent(
entity.domain, "close_cover_tilt", entity.entityId, null));
}
void _stop(CoverEntity entity) {
eventBus.fire(new ServiceCallEvent(
entity.domain, "stop_cover_tilt", entity.entityId, null));
}
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final CoverEntity entity = entityModel.entity;
List<Widget> buttons = [];
if (entity.supportOpenTilt) {
buttons.add(IconButton(
icon: Icon(
MaterialDesignIcons.createIconDataFromIconName(
"mdi:arrow-top-right"),
size: Entity.iconSize,
),
onPressed: entity.canTiltBeOpened ? () => _open(entity) : null));
} else {
buttons.add(Container(
width: Entity.iconSize + 20.0,
));
}
if (entity.supportStopTilt) {
buttons.add(IconButton(
icon: Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:stop"),
size: Entity.iconSize,
),
onPressed: () => _stop(entity)));
} else {
buttons.add(Container(
width: Entity.iconSize + 20.0,
));
}
if (entity.supportCloseTilt) {
buttons.add(IconButton(
icon: Icon(
MaterialDesignIcons.createIconDataFromIconName(
"mdi:arrow-bottom-left"),
size: Entity.iconSize,
),
onPressed: entity.canTiltBeClosed ? () => _close(entity) : null));
} else {
buttons.add(Container(
width: Entity.iconSize + 20.0,
));
}
return Row(
children: buttons,
);
}
}

View File

@ -0,0 +1,240 @@
part of '../../main.dart';
class LightControlsWidget extends StatefulWidget {
@override
_LightControlsWidgetState createState() => _LightControlsWidgetState();
}
class _LightControlsWidgetState extends State<LightControlsWidget> {
int _tmpBrightness;
int _tmpColorTemp;
Color _tmpColor;
bool _changedHere = false;
String _tmpEffect;
void _resetState(LightEntity entity) {
_tmpBrightness = entity.brightness ?? 0;
_tmpColorTemp = entity.colorTemp;
_tmpColor = entity.color;
_tmpEffect = null;
}
void _setBrightness(LightEntity entity, double value) {
setState(() {
_tmpBrightness = value.round();
_changedHere = true;
if (_tmpBrightness > 0) {
eventBus.fire(new ServiceCallEvent(
entity.domain, "turn_on", entity.entityId,
{"brightness": _tmpBrightness}));
} else {
eventBus.fire(new ServiceCallEvent(
entity.domain, "turn_off", entity.entityId,
null));
}
});
}
void _setColorTemp(LightEntity entity, double value) {
setState(() {
_tmpColorTemp = value.round();
_changedHere = true;
eventBus.fire(new ServiceCallEvent(
entity.domain, "turn_on", entity.entityId,
{"color_temp": _tmpColorTemp}));
});
}
void _setColor(LightEntity entity, Color 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]}));
}
});
}
void _setEffect(LightEntity entity, String value) {
setState(() {
_tmpEffect = value;
_changedHere = true;
if (_tmpEffect != null) {
eventBus.fire(new ServiceCallEvent(
entity.domain, "turn_on", entity.entityId,
{"effect": "$value"}));
}
});
}
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final LightEntity entity = entityModel.entity;
if (!_changedHere) {
_resetState(entity);
} else {
_changedHere = false;
}
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
_buildBrightnessControl(entity),
_buildColorTempControl(entity),
_buildColorControl(entity),
_buildEffectControl(entity)
],
);
}
Widget _buildBrightnessControl(LightEntity entity) {
if ((entity.supportBrightness) && (_tmpBrightness != null)) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(height: Entity.rowPadding,),
Text(
"Brightness",
style: TextStyle(fontSize: Entity.stateFontSize),
),
Container(height: Entity.rowPadding,),
Row(
children: <Widget>[
Icon(Icons.brightness_5),
Expanded(
child: Slider(
value: _tmpBrightness.toDouble(),
min: 0.0,
max: 255.0,
onChanged: (value) {
setState(() {
_changedHere = true;
_tmpBrightness = value.round();
});
},
onChangeEnd: (value) => _setBrightness(entity, value),
),
)
],
),
Container(height: Entity.rowPadding,)
],
);
} else {
return Container(width: 0.0, height: 0.0);
}
}
Widget _buildColorTempControl(LightEntity entity) {
if ((entity.supportColorTemp) && (_tmpColorTemp != null)) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(height: Entity.rowPadding,),
Text(
"Color temperature",
style: TextStyle(fontSize: Entity.stateFontSize),
),
Container(height: Entity.rowPadding,),
Row(
children: <Widget>[
Text("Cold", style: TextStyle(color: Colors.lightBlue),),
Expanded(
child: Slider(
value: _tmpColorTemp.toDouble(),
min: entity.minMireds,
max: entity.maxMireds,
onChanged: (value) {
setState(() {
_changedHere = true;
_tmpColorTemp = value.round();
});
},
onChangeEnd: (value) => _setColorTemp(entity, value),
),
),
Text("Warm", style: TextStyle(color: Colors.amberAccent),),
],
),
Container(height: Entity.rowPadding,)
],
);
} else {
return Container(width: 0.0, height: 0.0);
}
}
Widget _buildColorControl(LightEntity entity) {
if ((entity.supportColor) && (entity.color != null)) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Container(height: Entity.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*Entity.rowPadding,),
],
);
} else {
return Container(width: 0.0, height: 0.0);
}
}
void _showColorPicker(LightEntity entity) {
showDialog(
context: context,
builder: (BuildContext context) {
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,
),
),
);
},
);
}
Widget _buildEffectControl(LightEntity entity) {
if ((entity.supportEffect) && (entity.effectList != null)) {
return ModeSelectorWidget(
onChange: (effect) => _setEffect(entity, effect),
caption: "Effect",
options: entity.effectList,
value: _tmpEffect
);
} else {
return Container(width: 0.0, height: 0.0);
}
}
}

View File

@ -0,0 +1,28 @@
part of '../main.dart';
class DefaultEntityContainer extends StatelessWidget {
DefaultEntityContainer({
Key key,
@required this.state,
@required this.height
}) : super(key: key);
final Widget state;
final double height;
@override
Widget build(BuildContext context) {
return SizedBox(
height: height,
child: Row(
children: <Widget>[
EntityIcon(),
Expanded(
child: EntityName(),
),
state
],
),
);
}
}

View File

@ -0,0 +1,57 @@
part of '../main.dart';
class EntityAttributesList extends StatelessWidget {
EntityAttributesList({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
List<Widget> attrs = [];
if ((entityModel.entity.attributesToShow == null) ||
(entityModel.entity.attributesToShow.contains("all"))) {
entityModel.entity.attributes.forEach((name, value) {
attrs.add(_buildSingleAttribute("$name", "$value"));
});
} else {
entityModel.entity.attributesToShow.forEach((String attr) {
String attrValue = entityModel.entity.getAttribute("$attr");
if (attrValue != null) {
attrs.add(
_buildSingleAttribute("$attr", "$attrValue"));
}
});
}
return Column(
children: attrs,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
);
}
Widget _buildSingleAttribute(String name, String value) {
return Row(
children: <Widget>[
Expanded(
child: Padding(
padding: EdgeInsets.fromLTRB(
Entity.leftWidgetPadding, Entity.rowPadding, 0.0, 0.0),
child: Text(
"$name",
textAlign: TextAlign.left,
),
),
),
Expanded(
child: Padding(
padding: EdgeInsets.fromLTRB(
0.0, Entity.rowPadding, Entity.rightWidgetPadding, 0.0),
child: Text(
"$value",
textAlign: TextAlign.right,
),
),
)
],
);
}
}

View File

@ -0,0 +1,54 @@
part of '../main.dart';
class EntityColors {
static const _stateColors = {
"on": Colors.amber,
"auto": Colors.amber,
"idle": Colors.amber,
"playing": Colors.amber,
"above_horizon": Colors.amber,
"home": Colors.amber,
"open": Colors.amber,
"off": Color.fromRGBO(68, 115, 158, 1.0),
"closed": Color.fromRGBO(68, 115, 158, 1.0),
"below_horizon": Color.fromRGBO(68, 115, 158, 1.0),
"default": Color.fromRGBO(68, 115, 158, 1.0),
"heat": Colors.redAccent,
"cool": Colors.lightBlue,
"unavailable": Colors.black26,
"unknown": Colors.black26,
};
static Color stateColor(String state) {
return _stateColors[state] ?? _stateColors["default"];
}
static charts.Color chartHistoryStateColor(String state, int id) {
Color c = _stateColors[state];
if (c != null) {
return charts.Color(
r: c.red,
g: c.green,
b: c.blue,
a: c.alpha
);
} else {
return charts.MaterialPalette.getOrderedPalettes(id+1)[id].shadeDefault;
}
}
static Color historyStateColor(String state, int id) {
Color c = _stateColors[state];
if (c != null) {
return c;
} else {
if (id > -1) {
charts.Color c1 = charts.MaterialPalette.getOrderedPalettes(id + 1)[id].shadeDefault;
return Color.fromARGB(c1.a, c1.r, c1.g, c1.b);
} else {
return _stateColors["on"];
}
}
}
}

View File

@ -0,0 +1,22 @@
part of '../main.dart';
class EntityIcon extends StatelessWidget {
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
return GestureDetector(
child: Padding(
padding: EdgeInsets.fromLTRB(
Entity.leftWidgetPadding, 0.0, 12.0, 0.0),
child: MaterialDesignIcons.createIconWidgetFromEntityData(
entityModel.entity,
Entity.iconSize,
EntityColors.stateColor(entityModel.entity.state)
),
),
onTap: () => entityModel.handleTap
? eventBus.fire(new ShowEntityPageEvent(entityModel.entity))
: null,
);
}
}

View File

@ -0,0 +1,23 @@
part of '../main.dart';
class EntityName extends StatelessWidget {
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
return GestureDetector(
child: Padding(
padding: EdgeInsets.only(right: 10.0),
child: Text(
"${entityModel.entity.displayName}",
overflow: TextOverflow.ellipsis,
softWrap: false,
style: TextStyle(fontSize: Entity.nameFontSize),
),
),
onTap: () =>
entityModel.handleTap
? eventBus.fire(new ShowEntityPageEvent(entityModel.entity))
: null,
);
}
}

View File

@ -0,0 +1,14 @@
part of '../main.dart';
class EntityPageContainer extends StatelessWidget {
EntityPageContainer({Key key, @required this.children}) : super(key: key);
final List<Widget> children;
@override
Widget build(BuildContext context) {
return ListView(
children: children,
);
}
}

View File

@ -0,0 +1,230 @@
part of '../../main.dart';
class CombinedHistoryChartWidget extends StatefulWidget {
final rawHistory;
final EntityHistoryConfig config;
const CombinedHistoryChartWidget({Key key, @required this.rawHistory, @required this.config}) : super(key: key);
@override
State<StatefulWidget> createState() {
return new _CombinedHistoryChartWidgetState();
}
}
class _CombinedHistoryChartWidgetState extends State<CombinedHistoryChartWidget> {
int _selectedId = -1;
List<charts.Series<EntityHistoryMoment, DateTime>> _parsedHistory;
@override
void initState() {
// TODO: implement initState
super.initState();
}
@override
Widget build(BuildContext context) {
_parsedHistory = _parseHistory();
DateTime selectedTime;
List<String> selectedStates = [];
List<int> colorIndexes = [];
if ((_selectedId > -1) && (_parsedHistory != null) && (_parsedHistory.first.data.length >= (_selectedId + 1))) {
selectedTime = _parsedHistory.first.data[_selectedId].startTime;
_parsedHistory.where((item) { return item.id == "state"; }).forEach((item) {
selectedStates.add(item.data[_selectedId].state);
colorIndexes.add(item.data[_selectedId].colorId);
});
_parsedHistory.where((item) { return item.id == "value"; }).forEach((item) {
selectedStates.add("${item.data[_selectedId].value ?? '-'}");
colorIndexes.add(item.data[_selectedId].colorId);
});
}
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
HistoryControlWidget(
selectedTimeStart: selectedTime,
selectedStates: selectedStates,
onPrevTap: () => _selectPrev(),
onNextTap: () => _selectNext(),
colorIndexes: colorIndexes,
),
SizedBox(
height: 150.0,
child: charts.TimeSeriesChart(
_parsedHistory,
animate: false,
primaryMeasureAxis: new charts.NumericAxisSpec(
tickProviderSpec:
new charts.BasicNumericTickProviderSpec(zeroBound: false)),
dateTimeFactory: const charts.LocalDateTimeFactory(),
defaultRenderer: charts.LineRendererConfig(
includeArea: false,
includePoints: true
),
selectionModels: [
new charts.SelectionModelConfig(
type: charts.SelectionModelType.info,
listener: (model) => _onSelectionChanged(model),
)
],
customSeriesRenderers: [
new charts.SymbolAnnotationRendererConfig(
customRendererId: "stateBars"
)
],
),
)
],
);
}
double _parseToDouble(temp1) {
if (temp1 is int) {
return temp1.toDouble();
} else if (temp1 is double) {
return temp1;
} else {
return double.tryParse("$temp1");
}
}
List<charts.Series<EntityHistoryMoment, DateTime>> _parseHistory() {
TheLogger.debug(" parsing history...");
Map<String, List<EntityHistoryMoment>> numericDataLists = {};
int colorIdCounter = 0;
widget.config.numericAttributesToShow.forEach((String attrName) {
TheLogger.debug(" parsing attribute $attrName");
List<EntityHistoryMoment> data = [];
DateTime now = DateTime.now();
for (var i = 0; i < widget.rawHistory.length; i++) {
var stateData = widget.rawHistory[i];
DateTime startTime = DateTime.tryParse(stateData["last_updated"])?.toLocal();
DateTime endTime;
bool hiddenLine;
double value;
double previousValue = 0.0;
value = _parseToDouble(stateData["attributes"]["$attrName"]);
bool hiddenDot = (value == null);
if (hiddenDot && i > 0) {
previousValue = data[i-1].value ?? data[i-1].previousValue;
}
if (i < (widget.rawHistory.length - 1)) {
endTime = DateTime.tryParse(widget.rawHistory[i+1]["last_updated"])?.toLocal();
double nextValue = _parseToDouble(widget.rawHistory[i+1]["attributes"]["$attrName"]);
hiddenLine = (nextValue == null || hiddenDot);
} else {
hiddenLine = hiddenDot;
endTime = now;
}
data.add(EntityHistoryMoment(
value: value,
previousValue: previousValue,
hiddenDot: hiddenDot,
hiddenLine: hiddenLine,
state: stateData["state"],
startTime: startTime,
endTime: endTime,
id: i,
colorId: colorIdCounter
));
}
data.add(EntityHistoryMoment(
value: data.last.value,
previousValue: data.last.previousValue,
hiddenDot: data.last.hiddenDot,
hiddenLine: data.last.hiddenLine,
state: data.last.state,
startTime: now,
id: widget.rawHistory.length,
colorId: colorIdCounter
));
numericDataLists.addAll({attrName: data});
colorIdCounter += 1;
});
if ((_selectedId == -1) && (numericDataLists.isNotEmpty)) {
_selectedId = 0;
}
List<charts.Series<EntityHistoryMoment, DateTime>> result = [];
numericDataLists.forEach((attrName, dataList) {
TheLogger.debug(" adding ${dataList.length} data values");
result.add(
new charts.Series<EntityHistoryMoment, DateTime>(
id: "value",
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColors.chartHistoryStateColor("_", historyMoment.colorId),
radiusPxFn: (EntityHistoryMoment historyMoment, __) {
if (historyMoment.hiddenDot) {
return 0.0;
} else if (historyMoment.id == _selectedId) {
return 5.0;
} else {
return 1.0;
}
},
strokeWidthPxFn: (EntityHistoryMoment historyMoment, __) => historyMoment.hiddenLine ? 0.0 : 2.0,
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
measureFn: (EntityHistoryMoment historyMoment, _) => historyMoment.value ?? historyMoment.previousValue,
data: dataList,
/*domainLowerBoundFn: (CombinedEntityStateHistoryMoment historyMoment, _) => historyMoment.time.subtract(Duration(hours: 1)),
domainUpperBoundFn: (CombinedEntityStateHistoryMoment historyMoment, _) => historyMoment.time.add(Duration(hours: 1)),*/
)
);
});
result.add(
new charts.Series<EntityHistoryMoment, DateTime>(
id: 'state',
radiusPxFn: (EntityHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 5.0 : 4.0,
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColors.chartHistoryStateColor(historyMoment.state, historyMoment.colorId),
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
domainLowerBoundFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
domainUpperBoundFn: (EntityHistoryMoment historyMoment, _) => historyMoment.endTime ?? DateTime.now(),
// No measure values are needed for symbol annotations.
measureFn: (_, __) => null,
data: numericDataLists[numericDataLists.keys.first],
)
// Configure our custom symbol annotation renderer for this series.
..setAttribute(charts.rendererIdKey, 'stateBars')
// Optional radius for the annotation shape. If not specified, this will
// default to the same radius as the points.
//..setAttribute(charts.boundsLineRadiusPxKey, 3.5)
);
return result;
}
void _selectPrev() {
if (_selectedId > 0) {
setState(() {
_selectedId -= 1;
});
}
}
void _selectNext() {
if (_selectedId < (_parsedHistory.first.data.length - 1)) {
setState(() {
_selectedId += 1;
});
}
}
void _onSelectionChanged(charts.SelectionModel model) {
final selectedDatum = model.selectedDatum;
int selectedId;
if (selectedDatum.isNotEmpty) {
selectedId = selectedDatum.first.datum.id;
setState(() {
_selectedId = selectedId;
});
} else {
setState(() {
});
}
}
}

View File

@ -0,0 +1,130 @@
part of '../../main.dart';
class EntityHistoryWidgetType {
static const int simple = 0;
static const int numericState = 1;
static const int numericAttributes = 2;
}
class EntityHistoryConfig {
final int chartType;
final List<String> numericAttributesToShow;
final bool numericState;
EntityHistoryConfig({this.chartType, this.numericAttributesToShow, this.numericState: true});
}
class EntityHistoryWidget extends StatefulWidget {
final EntityHistoryConfig config;
const EntityHistoryWidget({Key key, @required this.config}) : super(key: key);
@override
_EntityHistoryWidgetState createState() {
return new _EntityHistoryWidgetState();
}
}
class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
List _history;
bool _needToUpdateHistory;
@override
void initState() {
super.initState();
_needToUpdateHistory = true;
}
void _loadHistory(HomeAssistant ha, String entityId) {
ha.getHistory(entityId).then((history){
setState(() {
_history = history.isNotEmpty ? history[0] : [];
_needToUpdateHistory = false;
});
}).catchError((e) {
TheLogger.error("Error loading $entityId history: $e");
setState(() {
_history = [];
_needToUpdateHistory = false;
});
});
}
@override
Widget build(BuildContext context) {
final HomeAssistantModel homeAssistantModel = HomeAssistantModel.of(context);
final EntityModel entityModel = EntityModel.of(context);
final Entity entity = entityModel.entity;
if (!_needToUpdateHistory) {
_needToUpdateHistory = true;
} else {
_loadHistory(homeAssistantModel.homeAssistant, entity.entityId);
}
return _buildChart();
}
Widget _buildChart() {
List<Widget> children = [];
if (_history == null) {
children.add(
Text("Loading history...")
);
} else if (_history.isEmpty) {
children.add(
Text("No history")
);
} else {
children.add(
_selectChartWidget()
);
}
children.add(Divider());
return Padding(
padding: EdgeInsets.fromLTRB(0.0, 0.0, 0.0, Entity.rowPadding),
child: Column(
children: children,
),
);
}
Widget _selectChartWidget() {
TheLogger.debug(" selecting history widget (${widget.config.chartType})");
switch (widget.config.chartType) {
case EntityHistoryWidgetType.simple: {
TheLogger.debug(" Simple selected");
return SimpleStateHistoryChartWidget(
rawHistory: _history,
);
}
case EntityHistoryWidgetType.numericState: {
TheLogger.debug(" EntityHistory selected");
return NumericStateHistoryChartWidget(
rawHistory: _history,
config: widget.config,
);
}
case EntityHistoryWidgetType.numericAttributes: {
TheLogger.debug(" NumericAttributes selected");
return CombinedHistoryChartWidget(
rawHistory: _history,
config: widget.config,
);
}
default: {
TheLogger.debug(" Simple selected as default");
return SimpleStateHistoryChartWidget(
rawHistory: _history,
);
}
}
}
}

View File

@ -0,0 +1,25 @@
part of '../../main.dart';
class EntityHistoryMoment {
final DateTime startTime;
final DateTime endTime;
final double value;
final double previousValue;
final int id;
final int colorId;
final String state;
final bool hiddenDot;
final bool hiddenLine;
EntityHistoryMoment({
this.value,
this.previousValue,
this.hiddenDot,
this.hiddenLine,
this.state,
@required this.startTime,
this.endTime,
@required this.id,
this.colorId
});
}

View File

@ -0,0 +1,86 @@
part of '../../main.dart';
class HistoryControlWidget extends StatelessWidget {
final Function onPrevTap;
final Function onNextTap;
final DateTime selectedTimeStart;
final DateTime selectedTimeEnd;
final List<String> selectedStates;
final List<int> colorIndexes;
const HistoryControlWidget({Key key, this.onPrevTap, this.onNextTap, this.selectedTimeStart, this.selectedTimeEnd, this.selectedStates, @ required this.colorIndexes}) : super(key: key);
@override
Widget build(BuildContext context) {
if (selectedTimeStart != null) {
return
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
IconButton(
icon: Icon(Icons.chevron_left),
padding: EdgeInsets.all(0.0),
iconSize: 40.0,
onPressed: onPrevTap,
),
Expanded(
child: Padding(
padding: EdgeInsets.only(right: 10.0),
child: _buildStates(),
),
),
_buildTime(),
IconButton(
icon: Icon(Icons.chevron_right),
padding: EdgeInsets.all(0.0),
iconSize: 40.0,
onPressed: onNextTap,
),
],
);
} else {
return Container(height: 48.0);
}
}
Widget _buildStates() {
List<Widget> children = [];
for (int i = 0; i < selectedStates.length; i++) {
children.add(
Text(
"${selectedStates[i] ?? '-'}",
textAlign: TextAlign.right,
style: TextStyle(
fontWeight: FontWeight.bold,
color: EntityColors.historyStateColor(selectedStates[i], colorIndexes[i]),
fontSize: 22.0
),
)
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: children,
);
}
Widget _buildTime() {
List<Widget> children = [];
children.add(
Text("${formatDate(selectedTimeStart, [M, ' ', d, ', ', HH, ':', nn, ':', ss])}", textAlign: TextAlign.left,)
);
if (selectedTimeEnd != null) {
children.add(
Text("${formatDate(selectedTimeEnd, [M, ' ', d, ', ', HH, ':', nn, ':', ss])}", textAlign: TextAlign.left,)
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
);
}
}

View File

@ -0,0 +1,160 @@
part of '../../main.dart';
class NumericStateHistoryChartWidget extends StatefulWidget {
final rawHistory;
final EntityHistoryConfig config;
const NumericStateHistoryChartWidget({Key key, @required this.rawHistory, @required this.config}) : super(key: key);
@override
State<StatefulWidget> createState() {
return new _NumericStateHistoryChartWidgetState();
}
}
class _NumericStateHistoryChartWidgetState extends State<NumericStateHistoryChartWidget> {
int _selectedId = -1;
List<charts.Series<EntityHistoryMoment, DateTime>> _parsedHistory;
@override
Widget build(BuildContext context) {
_parsedHistory = _parseHistory();
DateTime selectedTime;
double selectedState;
if ((_selectedId > -1) && (_parsedHistory != null) && (_parsedHistory.first.data.length >= (_selectedId + 1))) {
selectedTime = _parsedHistory.first.data[_selectedId].startTime;
selectedState = _parsedHistory.first.data[_selectedId].value;
}
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
HistoryControlWidget(
selectedTimeStart: selectedTime,
selectedStates: ["${selectedState ?? '-'}"],
onPrevTap: () => _selectPrev(),
onNextTap: () => _selectNext(),
colorIndexes: [-1],
),
SizedBox(
height: 150.0,
child: charts.TimeSeriesChart(
_parsedHistory,
animate: false,
primaryMeasureAxis: new charts.NumericAxisSpec(
tickProviderSpec:
new charts.BasicNumericTickProviderSpec(zeroBound: false)),
dateTimeFactory: const charts.LocalDateTimeFactory(),
defaultRenderer: charts.LineRendererConfig(
includePoints: true
),
/*primaryMeasureAxis: charts.NumericAxisSpec(
renderSpec: charts.NoneRenderSpec()
),*/
selectionModels: [
new charts.SelectionModelConfig(
type: charts.SelectionModelType.info,
listener: (model) => _onSelectionChanged(model),
)
],
),
)
],
);
}
List<charts.Series<EntityHistoryMoment, DateTime>> _parseHistory() {
List<EntityHistoryMoment> data = [];
DateTime now = DateTime.now();
for (var i = 0; i < widget.rawHistory.length; i++) {
var stateData = widget.rawHistory[i];
DateTime time = DateTime.tryParse(stateData["last_updated"])?.toLocal();
double value = double.tryParse(stateData["state"]);
double previousValue = 0.0;
bool hiddenDot = (value == null);
bool hiddenLine;
if (hiddenDot && i > 0) {
previousValue = data[i-1].value ?? data[i-1].previousValue;
}
if (i < (widget.rawHistory.length - 1)) {
double nextValue = double.tryParse(widget.rawHistory[i+1]["state"]);
hiddenLine = (nextValue == null || hiddenDot);
} else {
hiddenLine = hiddenDot;
}
data.add(EntityHistoryMoment(
value: value,
previousValue: previousValue,
hiddenDot: hiddenDot,
hiddenLine: hiddenLine,
startTime: time,
id: i
));
}
data.add(EntityHistoryMoment(
value: data.last.value,
previousValue: data.last.previousValue,
hiddenDot: data.last.hiddenDot,
hiddenLine: data.last.hiddenLine,
startTime: now,
id: widget.rawHistory.length
));
if (_selectedId == -1) {
_selectedId = 0;
}
return [
new charts.Series<EntityHistoryMoment, DateTime>(
id: 'State',
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColors.chartHistoryStateColor("on", -1),
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
measureFn: (EntityHistoryMoment historyMoment, _) => historyMoment.value ?? historyMoment.previousValue,
data: data,
strokeWidthPxFn: (EntityHistoryMoment historyMoment, __) => historyMoment.hiddenLine ? 0.0 : 2.0,
radiusPxFn: (EntityHistoryMoment historyMoment, __) {
if (historyMoment.hiddenDot) {
return 0.0;
} else if (historyMoment.id == _selectedId) {
return 5.0;
} else {
return 1.0;
}
},
)
];
}
void _selectPrev() {
if (_selectedId > 0) {
setState(() {
_selectedId -= 1;
});
}
}
void _selectNext() {
if (_selectedId < (_parsedHistory.first.data.length - 1)) {
setState(() {
_selectedId += 1;
});
}
}
void _onSelectionChanged(charts.SelectionModel model) {
final selectedDatum = model.selectedDatum;
int selectedId;
if (selectedDatum.isNotEmpty) {
selectedId = selectedDatum.first.datum.id;
setState(() {
_selectedId = selectedId;
});
} else {
setState(() {
});
}
}
}

View File

@ -0,0 +1,176 @@
part of '../../main.dart';
class SimpleStateHistoryChartWidget extends StatefulWidget {
final rawHistory;
const SimpleStateHistoryChartWidget({Key key, this.rawHistory}) : super(key: key);
@override
State<StatefulWidget> createState() {
return new _SimpleStateHistoryChartWidgetState();
}
}
class _SimpleStateHistoryChartWidgetState extends State<SimpleStateHistoryChartWidget> {
int _selectedId = -1;
List<charts.Series<EntityHistoryMoment, DateTime>> _parsedHistory;
@override
Widget build(BuildContext context) {
_parsedHistory = _parseHistory();
DateTime selectedTimeStart;
DateTime selectedTimeEnd;
String selectedState;
if ((_selectedId > -1) && (_parsedHistory != null) && (_parsedHistory.first.data.length >= (_selectedId + 1))) {
selectedTimeStart = _parsedHistory.first.data[_selectedId].startTime;
selectedTimeEnd = _parsedHistory.first.data[_selectedId].endTime;
selectedState = _parsedHistory.first.data[_selectedId].state;
}
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
HistoryControlWidget(
selectedTimeStart: selectedTimeStart,
selectedTimeEnd: selectedTimeEnd,
selectedStates: [selectedState],
onPrevTap: () => _selectPrev(),
onNextTap: () => _selectNext(),
colorIndexes: [_parsedHistory.first.data[_selectedId].colorId],
),
SizedBox(
height: 70.0,
child: charts.TimeSeriesChart(
_parsedHistory,
animate: false,
dateTimeFactory: const charts.LocalDateTimeFactory(),
primaryMeasureAxis: charts.NumericAxisSpec(
renderSpec: charts.NoneRenderSpec()
),
selectionModels: [
new charts.SelectionModelConfig(
type: charts.SelectionModelType.info,
listener: (model) => _onSelectionChanged(model),
)
],
customSeriesRenderers: [
new charts.PointRendererConfig(
// ID used to link series to this renderer.
customRendererId: 'startValuePoints'),
new charts.PointRendererConfig(
// ID used to link series to this renderer.
customRendererId: 'endValuePoints')
],
),
)
],
);
}
List<charts.Series<EntityHistoryMoment, DateTime>> _parseHistory() {
List<EntityHistoryMoment> data = [];
DateTime now = DateTime.now();
Map<String, int> cachedStates = {};
for (var i = 0; i < widget.rawHistory.length; i++) {
var stateData = widget.rawHistory[i];
DateTime startTime = DateTime.tryParse(stateData["last_updated"])?.toLocal();
DateTime endTime;
if (i < (widget.rawHistory.length - 1)) {
endTime = DateTime.tryParse(widget.rawHistory[i+1]["last_updated"])?.toLocal();
} else {
endTime = now;
}
if (cachedStates[stateData["state"]] == null) {
cachedStates.addAll({"${stateData["state"]}": cachedStates.length});
}
data.add(EntityHistoryMoment(
state: stateData["state"],
startTime: startTime,
endTime: endTime,
id: i,
colorId: cachedStates[stateData["state"]]
));
}
data.add(EntityHistoryMoment(
state: data.last.state,
startTime: now,
id: widget.rawHistory.length,
colorId: data.last.colorId
));
if (_selectedId == -1) {
_selectedId = 0;
}
return [
new charts.Series<EntityHistoryMoment, DateTime>(
id: 'State',
strokeWidthPxFn: (EntityHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 6.0 : 3.0,
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColors.chartHistoryStateColor(historyMoment.state, historyMoment.colorId),
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
measureFn: (EntityHistoryMoment historyMoment, _) => 10,
data: data,
),
new charts.Series<EntityHistoryMoment, DateTime>(
id: 'State',
radiusPxFn: (EntityHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 5.0 : 3.0,
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColors.chartHistoryStateColor(historyMoment.state, historyMoment.colorId),
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
measureFn: (EntityHistoryMoment historyMoment, _) => 10,
data: data,
)..setAttribute(charts.rendererIdKey, 'startValuePoints'),
new charts.Series<EntityHistoryMoment, DateTime>(
id: 'State',
radiusPxFn: (EntityHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 5.0 : 3.0,
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColors.chartHistoryStateColor(historyMoment.state, historyMoment.colorId),
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.endTime ?? DateTime.now(),
measureFn: (EntityHistoryMoment historyMoment, _) => 10,
data: data,
)..setAttribute(charts.rendererIdKey, 'endValuePoints')
];
}
void _selectPrev() {
if (_selectedId > 0) {
setState(() {
_selectedId -= 1;
});
}
}
void _selectNext() {
if (_selectedId < (_parsedHistory.first.data.length - 2)) {
setState(() {
_selectedId += 1;
});
}
}
void _onSelectionChanged(charts.SelectionModel model) {
final selectedDatum = model.selectedDatum;
int selectedId;
if ((selectedDatum.isNotEmpty) &&(selectedDatum.first.datum.endTime != null)) {
selectedId = selectedDatum.first.datum.id;
setState(() {
_selectedId = selectedId;
});
} else {
setState(() {
});
}
}
}
/*
class SimpleEntityStateHistoryMoment {
final DateTime startTime;
final DateTime endTime;
final String state;
final int id;
final int colorId;
SimpleEntityStateHistoryMoment(this.state, this.startTime, this.endTime, this.id, this.colorId);
}*/

View File

@ -0,0 +1,18 @@
part of '../main.dart';
class LastUpdatedWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
return Padding(
padding: EdgeInsets.fromLTRB(
Entity.leftWidgetPadding, 0.0, 0.0, 0.0),
child: Text(
'${entityModel.entity.lastUpdated}',
textAlign: TextAlign.left,
style: TextStyle(
fontSize: Entity.smallFontSize, color: Colors.black26),
),
);
}
}

View File

@ -0,0 +1,62 @@
part of '../main.dart';
class ModeSelectorWidget extends StatelessWidget {
final String caption;
final List<String> options;
final String value;
final double captionFontSize;
final double valueFontSize;
final double bottomPadding;
final onChange;
ModeSelectorWidget({
Key key,
this.caption,
@required this.options,
this.value,
@required this.onChange,
this.captionFontSize,
this.valueFontSize,
this.bottomPadding
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text("$caption", style: TextStyle(
fontSize: captionFontSize ?? Entity.stateFontSize
)),
Row(
children: <Widget>[
Expanded(
child: ButtonTheme(
alignedDropdown: true,
child: DropdownButton<String>(
value: value,
iconSize: 30.0,
isExpanded: true,
style: TextStyle(
fontSize: valueFontSize ?? Entity.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),
),
),
)
],
),
Container(height: bottomPadding ?? Entity.rowPadding,)
],
);
}
}

View File

@ -0,0 +1,38 @@
part of '../main.dart';
class ModeSwitchWidget extends StatelessWidget {
final String caption;
final onChange;
final double captionFontSize;
final bool value;
ModeSwitchWidget({
Key key,
@required this.caption,
@required this.onChange,
this.captionFontSize,
this.value
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Expanded(
child: Text(
"$caption",
style: TextStyle(
fontSize: captionFontSize ?? Entity.stateFontSize
),
),
),
Switch(
onChanged: (value) => onChange(value),
value: value ?? false,
)
],
);
}
}

View File

@ -0,0 +1,42 @@
part of '../main.dart';
class EntityModel extends InheritedWidget {
const EntityModel({
Key key,
@required this.entity,
@required this.handleTap,
@required Widget child,
}) : super(key: key, child: child);
final Entity entity;
final bool handleTap;
static EntityModel of(BuildContext context) {
return context.inheritFromWidgetOfExactType(EntityModel);
}
@override
bool updateShouldNotify(InheritedWidget oldWidget) {
return true;
}
}
class HomeAssistantModel extends InheritedWidget {
const HomeAssistantModel({
Key key,
@required this.homeAssistant,
@required Widget child,
}) : super(key: key, child: child);
final HomeAssistant homeAssistant;
static HomeAssistantModel of(BuildContext context) {
return context.inheritFromWidgetOfExactType(HomeAssistantModel);
}
@override
bool updateShouldNotify(InheritedWidget oldWidget) {
return true;
}
}

View File

@ -0,0 +1,24 @@
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 FlatButton(
onPressed: (() {
_setNewState(entityModel.entity);
}),
child: Text(
"EXECUTE",
textAlign: TextAlign.right,
style:
new TextStyle(fontSize: Entity.stateFontSize, color: Colors.blue),
),
);
}
}

View File

@ -0,0 +1,57 @@
part of '../../main.dart';
class ClimateStateWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final ClimateEntity entity = entityModel.entity;
String targetTemp = "-";
if ((entity.supportTargetTemperature) && (entity.temperature != null)) {
targetTemp = "${entity.temperature}";
} else if ((entity.supportTargetTemperatureLow) &&
(entity.targetLow != null)) {
targetTemp = "${entity.targetLow}";
if ((entity.supportTargetTemperatureHigh) &&
(entity.targetHigh != null)) {
targetTemp += " - ${entity.targetHigh}";
}
}
return Padding(
padding: EdgeInsets.fromLTRB(
0.0, 0.0, Entity.rightWidgetPadding, 0.0),
child: GestureDetector(
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Row(
children: <Widget>[
Text("${entity.state}",
textAlign: TextAlign.right,
style: new TextStyle(
fontWeight: FontWeight.bold,
fontSize: Entity.stateFontSize,
)),
Text(" $targetTemp",
textAlign: TextAlign.right,
style: new TextStyle(
fontSize: Entity.stateFontSize,
))
],
),
entity.attributes["current_temperature"] != null ?
Text("Currently: ${entity.attributes["current_temperature"]}",
textAlign: TextAlign.right,
style: new TextStyle(
fontSize: Entity.stateFontSize,
color: Colors.black45)
) :
Container(height: 0.0,)
],
),
onTap: () => entityModel.handleTap
? eventBus.fire(new ShowEntityPageEvent(entity))
: null,
));
}
}

View File

@ -0,0 +1,65 @@
part of '../../main.dart';
class CoverStateWidget extends StatelessWidget {
void _open(CoverEntity entity) {
eventBus.fire(new ServiceCallEvent(
entity.domain, "open_cover", entity.entityId, null));
}
void _close(CoverEntity entity) {
eventBus.fire(new ServiceCallEvent(
entity.domain, "close_cover", entity.entityId, null));
}
void _stop(CoverEntity entity) {
eventBus.fire(new ServiceCallEvent(
entity.domain, "stop_cover", entity.entityId, null));
}
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final CoverEntity entity = entityModel.entity;
List<Widget> buttons = [];
if (entity.supportOpen) {
buttons.add(IconButton(
icon: Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:arrow-up"),
size: Entity.iconSize,
),
onPressed: entity.canBeOpened ? () => _open(entity) : null));
} else {
buttons.add(Container(
width: Entity.iconSize + 20.0,
));
}
if (entity.supportStop) {
buttons.add(IconButton(
icon: Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:stop"),
size: Entity.iconSize,
),
onPressed: () => _stop(entity)));
} else {
buttons.add(Container(
width: Entity.iconSize + 20.0,
));
}
if (entity.supportClose) {
buttons.add(IconButton(
icon: Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:arrow-down"),
size: Entity.iconSize,
),
onPressed: entity.canBeClosed ? () => _close(entity) : null));
} else {
buttons.add(Container(
width: Entity.iconSize + 20.0,
));
}
return Row(
children: buttons,
);
}
}

View File

@ -0,0 +1,75 @@
part of '../../main.dart';
class DateTimeStateWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final DateTimeEntity entity = entityModel.entity;
return Padding(
padding: EdgeInsets.fromLTRB(0.0, 0.0, Entity.rightWidgetPadding, 0.0),
child: GestureDetector(
child: Text("${entity.formattedState}",
textAlign: TextAlign.right,
style: new TextStyle(
fontSize: Entity.stateFontSize,
)),
onTap: () => _handleStateTap(context, entity),
));
}
void _handleStateTap(BuildContext context, DateTimeEntity entity) {
if (entity.hasDate) {
_showDatePicker(context, entity).then((date) {
if (date != null) {
if (entity.hasTime) {
_showTimePicker(context, entity).then((time) {
entity.setNewState({
"date": "${formatDate(date, [yyyy, '-', mm, '-', dd])}",
"time":
"${formatDate(DateTime(1970, 1, 1, time.hour, time.minute), [
HH,
':',
nn
])}"
});
});
} else {
entity.setNewState({
"date": "${formatDate(date, [yyyy, '-', mm, '-', dd])}"
});
}
}
});
} else if (entity.hasTime) {
_showTimePicker(context, entity).then((time) {
if (time != null) {
entity.setNewState({
"time":
"${formatDate(DateTime(1970, 1, 1, time.hour, time.minute), [
HH,
':',
nn
])}"
});
}
});
} else {
TheLogger.warning( "${entity.entityId} has no date and no time");
}
}
Future _showDatePicker(BuildContext context, DateTimeEntity entity) {
return showDatePicker(
context: context,
initialDate: entity.dateTimeState,
firstDate: DateTime(1970),
lastDate: DateTime(2037) //Unix timestamp will finish at Jan 19, 2038
);
}
Future _showTimePicker(BuildContext context, DateTimeEntity entity) {
return showTimePicker(
context: context,
initialTime: TimeOfDay.fromDateTime(entity.dateTimeState));
}
}

View File

@ -0,0 +1,46 @@
part of '../../main.dart';
class SelectStateWidget extends StatefulWidget {
SelectStateWidget({Key key}) : super(key: key);
@override
_SelectStateWidgetState createState() => _SelectStateWidgetState();
}
class _SelectStateWidgetState extends State<SelectStateWidget> {
void setNewState(domain, entityId, newValue) {
eventBus.fire(new ServiceCallEvent(domain, "select_option", entityId,
{"option": "$newValue"}));
}
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final SelectEntity entity = entityModel.entity;
Widget ctrl;
if (entity.listOptions.isNotEmpty) {
ctrl = DropdownButton<String>(
value: entity.state,
items: entity.listOptions.map((String value) {
return new DropdownMenuItem<String>(
value: value,
child: new Text(value),
);
}).toList(),
onChanged: (_) {
setNewState(entity.domain, entity.entityId,_);
},
);
} else {
ctrl = Text('---');
}
return Expanded(
//width: Entity.INPUT_WIDTH,
child: ctrl,
);
}
}

View File

@ -0,0 +1,22 @@
part of '../../main.dart';
class SimpleEntityState extends StatelessWidget {
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
return Padding(
padding: EdgeInsets.fromLTRB(
0.0, 0.0, Entity.rightWidgetPadding, 0.0),
child: GestureDetector(
child: Text(
"${entityModel.entity.state}${entityModel.entity.unitOfMeasurement}",
textAlign: TextAlign.right,
style: new TextStyle(
fontSize: Entity.stateFontSize,
)),
onTap: () => entityModel.handleTap
? eventBus.fire(new ShowEntityPageEvent(entityModel.entity))
: null,
));
}
}

View File

@ -0,0 +1,58 @@
part of '../../main.dart';
class SliderStateWidget extends StatefulWidget {
final bool expanded;
SliderStateWidget({Key key, @required this.expanded}) : super(key: key);
@override
_SliderStateWidgetState createState() => _SliderStateWidgetState();
}
class _SliderStateWidgetState extends State<SliderStateWidget> {
int _multiplier = 1;
void setNewState(newValue, domain, entityId) {
eventBus.fire(new ServiceCallEvent(domain, "set_value", entityId,
{"value": "${newValue.toString()}"}));
}
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final SliderEntity entity = entityModel.entity;
if (entity.valueStep < 1) {
_multiplier = 10;
} else if (entity.valueStep < 0.1) {
_multiplier = 100;
}
Widget slider = Slider(
min: entity.minValue * _multiplier,
max: entity.maxValue * _multiplier,
value: (entity.doubleState <= entity.maxValue) &&
(entity.doubleState >= entity.minValue)
? entity.doubleState * _multiplier
: entity.minValue * _multiplier,
onChanged: (value) {
setState(() {
entity.state =
(value.roundToDouble() / _multiplier).toString();
});
eventBus.fire(new StateChangedEvent(entity.entityId,
(value.roundToDouble() / _multiplier).toString(), true));
},
onChangeEnd: (value) {
setNewState(value.roundToDouble() / _multiplier, entity.domain, entity.entityId);
},
);
if (widget.expanded) {
return Expanded(
child: slider,
);
} else {
return slider;
}
}
}

View File

@ -0,0 +1,60 @@
part of '../../main.dart';
class SwitchStateWidget extends StatefulWidget {
@override
_SwitchStateWidgetState createState() => _SwitchStateWidgetState();
}
class _SwitchStateWidgetState extends State<SwitchStateWidget> {
@override
void initState() {
super.initState();
}
void _setNewState(newValue, Entity entity) {
setState(() {
entity.assumedState = newValue ? 'on' : 'off';
});
Timer(Duration(seconds: 2), (){
setState(() {
entity.assumedState = entity.state;
});
});
eventBus.fire(new ServiceCallEvent(
entity.domain, (newValue as bool) ? "turn_on" : "turn_off", entity.entityId, null));
}
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final entity = entityModel.entity;
if ((entity.attributes["assumed_state"] == null) || (entity.attributes["assumed_state"] == false)) {
return Switch(
value: entity.assumedState == 'on',
onChanged: ((switchState) {
_setNewState(switchState, entity);
}),
);
} else {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
IconButton(
onPressed: () => _setNewState(false, entity),
icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:flash-off")),
color: entity.assumedState == 'on' ? Colors.black : Colors.blue,
iconSize: Entity.iconSize,
),
IconButton(
onPressed: () => _setNewState(true, entity),
icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:flash")),
color: entity.assumedState == 'on' ? Colors.blue : Colors.black,
iconSize: Entity.iconSize
)
],
);
}
}
}

View File

@ -0,0 +1,98 @@
part of '../../main.dart';
class TextInputStateWidget extends StatefulWidget {
TextInputStateWidget({Key key}) : super(key: key);
@override
_TextInputStateWidgetState createState() => _TextInputStateWidgetState();
}
class _TextInputStateWidgetState extends State<TextInputStateWidget> {
String _tmpValue;
String _entityState;
String _entityDomain;
String _entityId;
int _minLength;
int _maxLength;
FocusNode _focusNode = FocusNode();
bool validValue = false;
@override
void initState() {
super.initState();
_focusNode.addListener(_focusListener);
}
void setNewState(newValue, domain, entityId) {
if (validate(newValue, _minLength, _maxLength)) {
eventBus.fire(new ServiceCallEvent(domain, "set_value", entityId,
{"value": "$newValue"}));
} else {
setState(() {
_tmpValue = _entityState;
});
}
}
bool validate(newValue, minLength, maxLength) {
if (newValue is String) {
validValue = (newValue.length >= minLength) &&
(maxLength == -1 ||
(newValue.length <= maxLength));
} else {
validValue = true;
}
return validValue;
}
void _focusListener() {
if (!_focusNode.hasFocus && (_tmpValue != _entityState)) {
setNewState(_tmpValue, _entityDomain, _entityId);
}
}
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final TextEntity entity = entityModel.entity;
_entityState = entity.state;
_entityDomain = entity.domain;
_entityId = entity.entityId;
_minLength = entity.valueMinLength;
_maxLength = entity.valueMaxLength;
if (!_focusNode.hasFocus && (_tmpValue != entity.state)) {
_tmpValue = entity.state;
}
if (entity.isTextField || entity.isPasswordField) {
return Expanded(
//width: Entity.INPUT_WIDTH,
child: TextField(
focusNode: _focusNode,
obscureText: entity.isPasswordField,
controller: new TextEditingController.fromValue(
new TextEditingValue(
text: _tmpValue,
selection:
new TextSelection.collapsed(offset: _tmpValue.length)
)
),
onChanged: (value) {
_tmpValue = value;
}),
);
} else {
TheLogger.warning( "Unsupported input mode for ${entity.entityId}");
return SimpleEntityState();
}
}
@override
void dispose() {
_focusNode.removeListener(_focusListener);
_focusNode.dispose();
super.dispose();
}
}

View File

@ -1,152 +1,252 @@
part of 'main.dart';
class HomeAssistant {
String _hassioAPIEndpoint;
String _hassioPassword;
String _hassioAuthType;
String _webSocketAPIEndpoint;
String _password;
String _authType;
bool _useLovelace;
IOWebSocketChannel _hassioChannel;
SendMessageQueue _messageQueue;
int _currentMessageId = 0;
int _statesMessageId = 0;
int _servicesMessageId = 0;
int _subscriptionMessageId = 0;
int _configMessageId = 0;
EntityCollection _entities;
UIBuilder _uiBuilder;
int _userInfoMessageId = 0;
int _lovelaceMessageId = 0;
EntityCollection entities;
HomeAssistantUI ui;
Map _instanceConfig = {};
String _userName;
Map _rawLovelaceData;
Completer _fetchCompleter;
Completer _statesCompleter;
Completer _servicesCompleter;
Completer _lovelaceCompleter;
Completer _configCompleter;
Timer _fetchingTimer;
Completer _connectionCompleter;
Completer _userInfoCompleter;
Timer _connectionTimer;
Timer _fetchTimer;
bool autoReconnect = false;
StreamSubscription _socketSubscription;
int messageExpirationTime = 30; //seconds
Duration fetchTimeout = Duration(seconds: 30);
Duration connectTimeout = Duration(seconds: 15);
String get locationName => _instanceConfig["location_name"] ?? "";
int get viewsCount => _entities.viewList.length ?? 0;
UIBuilder get uiBuilder => _uiBuilder;
String get userName => _userName ?? locationName;
String get userAvatarText => userName.length > 0 ? userName[0] : "";
//int get viewsCount => entities.views.length ?? 0;
EntityCollection get entities => _entities;
HomeAssistant() {
entities = EntityCollection();
_messageQueue = SendMessageQueue(messageExpirationTime);
}
HomeAssistant(String url, String password, String authType) {
_hassioAPIEndpoint = url;
_hassioPassword = password;
_hassioAuthType = authType;
_entities = EntityCollection();
_uiBuilder = UIBuilder();
void updateSettings(String url, String password, String authType, bool useLovelace) {
_webSocketAPIEndpoint = url;
_password = password;
_authType = authType;
_useLovelace = useLovelace;
TheLogger.debug( "Use lovelace is $_useLovelace");
}
Future fetch() {
if ((_fetchCompleter != null) && (!_fetchCompleter.isCompleted)) {
TheLogger.log("Warning","Previous fetch is not complited");
TheLogger.warning("Previous fetch is not complited");
} else {
//TODO: Fetch timeout timer. Should be removed after #21 fix
_fetchingTimer = Timer(Duration(seconds: 15), () {
closeConnection();
_fetchCompleter.completeError({"errorCode" : 1,"errorMessage": "Connection timeout"});
});
_fetchCompleter = new Completer();
_reConnectSocket().then((r) {
_fetchTimer = Timer(fetchTimeout, () {
TheLogger.error( "Data fetching timeout");
disconnect().then((_) {
_completeFetching({
"errorCode": 9,
"errorMessage": "Couldn't get data from server"
});
});
});
_connection().then((r) {
_getData();
}).catchError((e) {
_finishFetching(e);
_completeFetching(e);
});
}
return _fetchCompleter.future;
}
closeConnection() {
if (_hassioChannel?.closeCode == null) {
_hassioChannel?.sink?.close();
}
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")
);
await _socketSubscription.cancel();
_hassioChannel = null;
}
Future _reConnectSocket() {
var _connectionCompleter = new Completer();
if ((_hassioChannel == null) || (_hassioChannel.closeCode != null)) {
TheLogger.log("Debug","Socket connecting...");
_hassioChannel = IOWebSocketChannel.connect(_hassioAPIEndpoint);
_hassioChannel.stream.handleError((e) {
TheLogger.log("Error","Unhandled socket error: ${e.toString()}");
});
_hassioChannel.stream.listen((message) =>
_handleMessage(_connectionCompleter, message));
}
Future _connection() {
if ((_connectionCompleter != null) && (!_connectionCompleter.isCompleted)) {
TheLogger.debug("Previous connection is not complited");
} else {
_connectionCompleter.complete();
if ((_hassioChannel == null) || (_hassioChannel.closeCode != null)) {
_connectionCompleter = new Completer();
autoReconnect = false;
disconnect().then((_){
TheLogger.debug( "Socket connecting...");
_connectionTimer = Timer(connectTimeout, () {
TheLogger.error( "Socket connection timeout");
_handleSocketError(null);
});
if (_socketSubscription != null) {
_socketSubscription.cancel();
}
_hassioChannel = IOWebSocketChannel.connect(
_webSocketAPIEndpoint, pingInterval: Duration(seconds: 30));
_socketSubscription = _hassioChannel.stream.listen(
(message) => _handleMessage(message),
cancelOnError: true,
onDone: () => _handleSocketClose(),
onError: (e) => _handleSocketError(e)
);
});
} else {
_completeConnecting(null);
}
}
return _connectionCompleter.future;
}
_getData() {
_getConfig().then((result) {
_getStates().then((result) {
_getServices().then((result) {
_finishFetching(null);
}).catchError((e) {
_finishFetching(e);
void _handleSocketClose() {
TheLogger.debug("Socket disconnected. Automatic reconnect is $autoReconnect");
if (autoReconnect) {
_reconnect();
}
}
void _handleSocketError(e) {
TheLogger.error("Socket stream Error: $e");
TheLogger.debug("Automatic reconnect is $autoReconnect");
if (autoReconnect) {
_reconnect();
} else {
disconnect().then((_) {
_completeConnecting({
"errorCode": 1,
"errorMessage": "Couldn't connect to Home Assistant. Check network connection or connection settings."
});
}).catchError((e) {
_finishFetching(e);
});
}).catchError((e) {
_finishFetching(e);
}
}
void _reconnect() {
disconnect().then((_) {
_connection().catchError((e){
_completeConnecting(e);
});
});
}
_finishFetching(error) {
_fetchingTimer.cancel();
_getData() async {
List<Future> futures = [];
futures.add(_getStates());
if (_useLovelace) {
futures.add(_getLovelace());
}
futures.add(_getConfig());
futures.add(_getServices());
futures.add(_getUserInfo());
try {
await Future.wait(futures);
_createUI();
_completeFetching(null);
} catch (error) {
_completeFetching(error);
}
}
void _completeFetching(error) {
_fetchTimer.cancel();
_completeConnecting(error);
if (!_fetchCompleter.isCompleted) {
if (error != null) {
_fetchCompleter.completeError(error);
} else {
autoReconnect = true;
TheLogger.debug( "Fetch complete successful");
_fetchCompleter.complete();
}
}
}
_handleMessage(Completer connectionCompleter, String message) {
void _completeConnecting(error) {
_connectionTimer.cancel();
if (!_connectionCompleter.isCompleted) {
if (error != null) {
_connectionCompleter.completeError(error);
} else {
_connectionCompleter.complete();
}
} else if (error != null) {
eventBus.fire(ShowErrorEvent(error["errorMessage"], error["errorCode"]));
}
}
_handleMessage(String message) {
var data = json.decode(message);
//TheLogger.log("Debug","[Received] => Message type: ${data['type']}");
if (data["type"] == "auth_required") {
_sendMessageRaw('{"type": "auth","$_hassioAuthType": "$_hassioPassword"}');
_sendAuthMessageRaw('{"type": "auth","$_authType": "$_password"}');
} else if (data["type"] == "auth_ok") {
_completeConnecting(null);
_sendSubscribe();
connectionCompleter.complete();
} else if (data["type"] == "auth_invalid") {
connectionCompleter.completeError({"errorCode": 6, "errorMessage": "${data["message"]}"});
_completeConnecting({"errorCode": 6, "errorMessage": "${data["message"]}"});
} else if (data["type"] == "result") {
if (data["id"] == _configMessageId) {
_parseConfig(data);
} else if (data["id"] == _statesMessageId) {
_parseEntities(data);
} else if (data["id"] == _lovelaceMessageId) {
_handleLovelace(data);
} else if (data["id"] == _servicesMessageId) {
_parseServices(data);
} else if (data["id"] == _userInfoMessageId) {
_parseUserInfo(data);
} else if (data["id"] == _currentMessageId) {
TheLogger.log("Debug","Request id:$_currentMessageId was successful");
TheLogger.debug("[Received] => Request id:$_currentMessageId was successful");
}
} 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"]}");
_handleEntityStateChange(data["event"]["data"]);
} else if (data["event"] != null) {
TheLogger.log("Warning","Unhandled event type: ${data["event"]["event_type"]}");
TheLogger.warning("Unhandled event type: ${data["event"]["event_type"]}");
} else {
TheLogger.log("Error","Event is null: $message");
TheLogger.error("Event is null: $message");
}
} else {
TheLogger.log("Warning","Unknown message type: $message");
TheLogger.warning("Unknown message type: $message");
}
}
void _sendSubscribe() {
_incrementMessageId();
_subscriptionMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_subscriptionMessageId, "type": "subscribe_events", "event_type": "state_changed"}');
_sendMessageRaw('{"id": $_subscriptionMessageId, "type": "subscribe_events", "event_type": "state_changed"}', false);
}
Future _getConfig() {
_configCompleter = new Completer();
_incrementMessageId();
_configMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_configMessageId, "type": "get_config"}');
_sendMessageRaw('{"id": $_configMessageId, "type": "get_config"}', false);
return _configCompleter.future;
}
@ -155,16 +255,34 @@ class HomeAssistant {
_statesCompleter = new Completer();
_incrementMessageId();
_statesMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_statesMessageId, "type": "get_states"}');
_sendMessageRaw('{"id": $_statesMessageId, "type": "get_states"}', false);
return _statesCompleter.future;
}
Future _getLovelace() {
_lovelaceCompleter = new Completer();
_incrementMessageId();
_lovelaceMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_lovelaceMessageId, "type": "lovelace/config"}', false);
return _lovelaceCompleter.future;
}
Future _getUserInfo() {
_userInfoCompleter = new Completer();
_incrementMessageId();
_userInfoMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_userInfoMessageId, "type": "auth/current_user"}', false);
return _userInfoCompleter.future;
}
Future _getServices() {
_servicesCompleter = new Completer();
_incrementMessageId();
_servicesMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_servicesMessageId, "type": "get_services"}');
_sendMessageRaw('{"id": $_servicesMessageId, "type": "get_services"}', false);
return _servicesCompleter.future;
}
@ -173,19 +291,51 @@ class HomeAssistant {
_currentMessageId += 1;
}
_sendMessageRaw(String message) {
if (message.indexOf('"type": "auth"') > 0) {
TheLogger.log("Debug", "[Sending] ==> auth request");
} else {
TheLogger.log("Debug", "[Sending] ==> $message");
}
void _sendAuthMessageRaw(String message) {
TheLogger.debug( "[Sending] ==> auth request");
_hassioChannel.sink.add(message);
}
_sendMessageRaw(String message, bool queued) {
var sendCompleter = Completer();
if (queued) _messageQueue.add(message);
_connection().then((r) {
_messageQueue.getActualMessages().forEach((message){
TheLogger.debug( "[Sending queued] ==> $message");
_hassioChannel.sink.add(message);
});
if (!queued) {
TheLogger.debug( "[Sending] ==> $message");
_hassioChannel.sink.add(message);
}
sendCompleter.complete();
}).catchError((e){
sendCompleter.completeError(e);
});
return sendCompleter.future;
}
Future callService(String domain, String service, String entityId, Map<String, dynamic> additionalParams) {
_incrementMessageId();
String message = '{"id": $_currentMessageId, "type": "call_service", "domain": "$domain", "service": "$service", "service_data": {"entity_id": "$entityId"';
if (additionalParams != null) {
additionalParams.forEach((name, value){
if ((value is double) || (value is int) || (value is List)) {
message += ', "$name" : $value';
} else {
message += ', "$name" : "$value"';
}
});
}
message += '}}';
return _sendMessageRaw(message, true);
}
void _handleEntityStateChange(Map eventData) {
TheLogger.log("Debug", "New state for ${eventData['entity_id']}");
_entities.updateState(eventData);
eventBus.fire(new StateChangedEvent(eventData["entity_id"], null, false));
//TheLogger.debug( "New state for ${eventData['entity_id']}");
Map data = Map.from(eventData);
entities.updateState(data);
eventBus.fire(new StateChangedEvent(data["entity_id"], null, false));
}
void _parseConfig(Map data) {
@ -197,30 +347,81 @@ class HomeAssistant {
}
}
void _parseUserInfo(Map data) {
if (data["success"] == true) {
_userName = data["result"]["name"];
} else {
_userName = null;
}
_userInfoCompleter.complete();
}
void _parseServices(response) {
_servicesCompleter.complete();
/*if (response["success"] == false) {
_servicesCompleter.completeError({"errorCode": 4, "errorMessage": response["error"]["message"]});
return;
}
try {
Map data = response["result"];
Map result = {};
TheLogger.log("Debug","Parsing ${data.length} Home Assistant service domains");
data.forEach((domain, services) {
result[domain] = Map.from(services);
services.forEach((serviceName, serviceData) {
if (_entitiesData.isExist("$domain.$serviceName")) {
result[domain].remove(serviceName);
void _handleLovelace(response) {
if (response["success"] == true) {
_rawLovelaceData = response["result"];
} else {
_rawLovelaceData = null;
}
_lovelaceCompleter.complete();
}
void _parseLovelace() {
ui = HomeAssistantUI();
TheLogger.debug("Parsing lovelace config");
TheLogger.debug("--Title: ${_rawLovelaceData["title"]}");
int viewCounter = 0;
TheLogger.debug("--Views count: ${_rawLovelaceData['views'].length}");
_rawLovelaceData["views"].forEach((rawView){
TheLogger.debug("----view id: ${rawView['id']}");
HAView view = HAView(
count: viewCounter,
id: rawView['id'],
name: rawView['title'],
iconName: rawView['icon']
);
view.cards.addAll(_createLovelaceCards(rawView["cards"] ?? []));
ui.views.add(
view
);
viewCounter += 1;
});
}
List<HACard> _createLovelaceCards(List rawCards) {
List<HACard> result = [];
rawCards.forEach((rawCard){
if (rawCard["cards"] != null) {
TheLogger.debug("------card: ${rawCard['type']} has child cards");
result.addAll(_createLovelaceCards(rawCard["cards"]));
} else {
TheLogger.debug("------card: ${rawCard['type']}");
HACard card = HACard(
id: "card",
name: rawCard["title"],
type: rawCard['type']
);
rawCard["entities"]?.forEach((rawEntity) {
if (rawEntity is String) {
if (entities.isExist(rawEntity)) {
card.entities.add(entities.get(rawEntity));
}
} else {
if (entities.isExist(rawEntity["entity"])) {
card.entities.add(entities.get(rawEntity["entity"]));
}
}
});
if (rawCard["entity"] != null) {
card.linkedEntity = entities.get(rawCard["entity"]);
}
result.add(card);
}
});
_servicesData = result;
_servicesCompleter.complete();
} catch (e) {
TheLogger.log("Error","Error parsing services. But they are not used :-)");
_servicesCompleter.complete();
}*/
return result;
}
void _parseEntities(response) async {
@ -228,33 +429,114 @@ class HomeAssistant {
_statesCompleter.completeError({"errorCode": 3, "errorMessage": response["error"]["message"]});
return;
}
_entities.parse(response["result"]);
_uiBuilder.build(_entities);
entities.parse(response["result"]);
_statesCompleter.complete();
}
Future callService(String domain, String service, String entityId, Map<String, String> additionalParams) {
var sendCompleter = Completer();
//TODO: Send service call timeout timer. Should be removed after #21 fix
Timer _sendTimer = Timer(Duration(seconds: 7), () {
sendCompleter.completeError({"errorCode" : 8,"errorMessage": "Connection timeout"});
});
_reConnectSocket().then((r) {
_incrementMessageId();
String message = '{"id": $_currentMessageId, "type": "call_service", "domain": "$domain", "service": "$service", "service_data": {"entity_id": "$entityId"';
if (additionalParams != null) {
additionalParams.forEach((name, value){
message += ', "$name" : "$value"';
void _createUI() {
if ((_useLovelace) && (_rawLovelaceData != null)) {
_parseLovelace();
} else {
ui = HomeAssistantUI();
int viewCounter = 0;
if (!entities.hasDefaultView) {
TheLogger.debug( "--Default view");
HAView view = HAView(
count: viewCounter,
id: "group.default_view",
name: "Home",
childEntities: entities.filterEntitiesForDefaultView()
);
ui.views.add(
view
);
viewCounter += 1;
}
entities.viewEntities.forEach((viewEntity) {
TheLogger.debug( "--View: ${viewEntity.entityId}");
HAView view = HAView(
count: viewCounter,
id: viewEntity.entityId,
name: viewEntity.displayName,
childEntities: viewEntity.childEntities
);
view.linkedEntity = viewEntity;
ui.views.add(
view
);
viewCounter += 1;
});
}
message += '}}';
_sendMessageRaw(message);
_sendTimer.cancel();
sendCompleter.complete();
}).catchError((e){
_sendTimer.cancel();
sendCompleter.completeError(e);
}
Widget buildViews(BuildContext context, bool lovelace) {
return ui.build(context);
}
Future<List> getHistory(String entityId) async {
DateTime now = DateTime.now();
//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( "$url");
http.Response historyResponse;
if (_authType == "access_token") {
historyResponse = await http.get(url, headers: {
"authorization": "Bearer $_password",
"Content-Type": "application/json"
});
return sendCompleter.future;
} 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( "Got ${history.first.length} history recors");
return history;
} else {
return [];
}
}
}
class SendMessageQueue {
int _messageTimeout;
List<HAMessage> _queue = [];
SendMessageQueue(this._messageTimeout);
void add(String message) {
_queue.add(HAMessage(_messageTimeout, message));
}
List<String> getActualMessages() {
_queue.removeWhere((item) => item.isExpired());
List<String> result = [];
_queue.forEach((haMessage){
result.add(haMessage.message);
});
this.clear();
return result;
}
void clear() {
_queue.clear();
}
}
class HAMessage {
DateTime _timeStamp;
int _messageTimeout;
String message;
HAMessage(this._messageTimeout, this.message) {
_timeStamp = DateTime.now();
}
bool isExpired() {
return DateTime.now().difference(_timeStamp).inSeconds > _messageTimeout;
}
}

View File

@ -51,7 +51,6 @@ class _LogViewPageState extends State<LogViewPage> {
),
body: TextField(
maxLines: null,
controller: TextEditingController(
text: _logData
),

View File

@ -11,15 +11,50 @@ import 'package:cached_network_image/cached_network_image.dart';
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;
part 'entity_class/entity.class.dart';
part 'entity_class/button_entity.class.dart';
part 'entity_class/datetime_entity.class.dart';
part 'entity_class/select_entity.class.dart';
part 'entity_class/slider_entity.class.dart';
part 'entity_class/switch_entity.class.dart';
part 'entity_class/button_entity.class.dart';
part 'entity_class/text_entity.class.dart';
part 'entity_class/climate_entity.class.dart';
part 'entity_class/cover_entity.class.dart';
part 'entity_class/date_time_entity.class.dart';
part 'entity_class/light_entity.class.dart';
part 'entity_class/select_entity.class.dart';
part 'entity_class/other_entity.class.dart';
part 'entity_class/slider_entity.dart';
part 'entity_widgets/badge.dart';
part 'entity_widgets/model_widgets.dart';
part 'entity_widgets/default_entity_container.dart';
part 'entity_widgets/entity_attributes_list.dart';
part 'entity_widgets/entity_icon.dart';
part 'entity_widgets/entity_name.dart';
part 'entity_widgets/last_updated.dart';
part 'entity_widgets/mode_swicth.dart';
part 'entity_widgets/mode_selector.dart';
part 'entity_widgets/entity_colors.class.dart';
part 'entity_widgets/entity_page_container.dart';
part 'entity_widgets/history_chart/entity_history.dart';
part 'entity_widgets/history_chart/simple_state_history_chart.dart';
part 'entity_widgets/history_chart/numeric_state_history_chart.dart';
part 'entity_widgets/history_chart/combined_history_chart.dart';
part 'entity_widgets/history_chart/history_control_widget.dart';
part 'entity_widgets/history_chart/entity_history_moment.dart';
part 'entity_widgets/state/switch_state.dart';
part 'entity_widgets/state/slider_state.dart';
part 'entity_widgets/state/text_input_state.dart';
part 'entity_widgets/state/select_state.dart';
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/controls/climate_controls.dart';
part 'entity_widgets/controls/cover_controls.dart';
part 'entity_widgets/controls/light_controls.dart';
part 'settings.page.dart';
part 'home_assistant.class.dart';
part 'log.page.dart';
@ -27,20 +62,25 @@ part 'entity.page.dart';
part 'utils.class.dart';
part 'mdi.class.dart';
part 'entity_collection.class.dart';
part 'ui_builder_class.dart';
part 'view_class.dart';
part 'card_class.dart';
part 'badge_class.dart';
part 'ui_class/ui.dart';
part 'ui_class/view.class.dart';
part 'ui_class/card.class.dart';
part 'ui_widgets/view.dart';
part 'ui_widgets/entities_card.dart';
part 'ui_widgets/unsupported_card.dart';
part 'ui_widgets/media_control_card.dart';
part 'ui_widgets/card_header_widget.dart';
EventBus eventBus = new EventBus();
const String appName = "HA Client";
const appVersion = "0.2.2";
const appVersion = "0.3.4";
String homeAssistantWebHost;
void main() {
FlutterError.onError = (errorDetails) {
TheLogger.log("Error", "${errorDetails.exception}");
TheLogger.error( "${errorDetails.exception}");
if (TheLogger.isInDebugMode) {
FlutterError.dumpErrorToConsole(errorDetails);
}
@ -49,7 +89,8 @@ void main() {
runZoned(() {
runApp(new HAClientApp());
}, onError: (error, stack) {
TheLogger.log("Global error", "$error");
TheLogger.error("$error");
TheLogger.error("$stack");
if (TheLogger.isInDebugMode) {
debugPrint("$stack");
}
@ -68,7 +109,7 @@ class HAClientApp extends StatelessWidget {
initialRoute: "/",
routes: {
"/": (context) => MainPage(title: 'HA Client'),
"/connection-settings": (context) => ConnectionSettingsPage(title: "Connection Settings"),
"/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"),
"/log-view": (context) => LogViewPage(title: "Log")
},
);
@ -88,67 +129,82 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
HomeAssistant _homeAssistant;
EntityCollection _entities;
//Map _instanceConfig;
int _uiViewsCount = 0;
String _webSocketApiEndpoint;
String _password;
String _authType;
//int _uiViewsCount = 0;
String _instanceHost;
int _errorCodeToBeShown = 0;
String _lastErrorMessage = "";
StreamSubscription _stateSubscription;
StreamSubscription _settingsSubscription;
StreamSubscription _serviceCallSubscription;
StreamSubscription _showEntityPageSubscription;
bool _isLoading = true;
Map<String, Color> _badgeColors = {
"default": Color.fromRGBO(223, 76, 30, 1.0),
"binary_sensor": Color.fromRGBO(3, 155, 229, 1.0)
};
StreamSubscription _refreshDataSubscription;
StreamSubscription _showErrorSubscription;
int _isLoading = 1;
bool _settingsLoaded = false;
bool _accountMenuExpanded = false;
bool _useLovelaceUI;
@override
void initState() {
super.initState();
_settingsLoaded = false;
WidgetsBinding.instance.addObserver(this);
_homeAssistant = HomeAssistant();
_settingsSubscription = eventBus.on<SettingsChangedEvent>().listen((event) {
TheLogger.log("Debug","Settings change event: reconnect=${event.reconnect}");
TheLogger.debug("Settings change event: reconnect=${event.reconnect}");
if (event.reconnect) {
_homeAssistant.disconnect().then((_){
_initialLoad();
});
}
});
_initialLoad();
}
void _initialLoad() {
_loadConnectionSettings().then((_){
_subscribe();
_refreshData();
}, onError: (_) {
setState(() {
_errorCodeToBeShown = 0;
_isLoading = 2;
});
_initConnection();
_showErrorSnackBar(message: _, errorCode: 5);
});
_initConnection();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
TheLogger.log("Debug","$state");
if (state == AppLifecycleState.resumed) {
TheLogger.debug("$state");
if (state == AppLifecycleState.resumed && _settingsLoaded) {
_refreshData();
}
}
_initConnection() async {
_loadConnectionSettings() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String domain = prefs.getString('hassio-domain');
String port = prefs.getString('hassio-port');
_instanceHost = "$domain:$port";
String apiEndpoint = "${prefs.getString('hassio-protocol')}://$domain:$port/api/websocket";
_webSocketApiEndpoint = "${prefs.getString('hassio-protocol')}://$domain:$port/api/websocket";
homeAssistantWebHost = "${prefs.getString('hassio-res-protocol')}://$domain:$port";
String apiPassword = prefs.getString('hassio-password');
String authType = prefs.getString('hassio-auth-type');
if ((domain == null) || (port == null) || (apiPassword == null) ||
(domain.length == 0) || (port.length == 0) || (apiPassword.length == 0)) {
setState(() {
_errorCodeToBeShown = 5;
});
_password = prefs.getString('hassio-password');
_authType = prefs.getString('hassio-auth-type');
_useLovelaceUI = prefs.getBool('use-lovelace') ?? false;
if ((domain == null) || (port == null) || (_password == null) ||
(domain.length == 0) || (port.length == 0) || (_password.length == 0)) {
throw("Check connection settings");
} else {
if (_homeAssistant != null) _homeAssistant.closeConnection();
_createConnection(apiEndpoint, apiPassword, authType);
_settingsLoaded = true;
}
}
_createConnection(String apiEndpoint, String apiPassword, String authType) {
_homeAssistant = HomeAssistant(apiEndpoint, apiPassword, authType);
_refreshData();
if (_stateSubscription != null) _stateSubscription.cancel();
_subscribe() {
if (_stateSubscription == null) {
//TODO Move to homeAssistant or remove
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
setState(() {
if (event.localChange) {
@ -158,286 +214,110 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
}
});
});
if (_serviceCallSubscription != null) _serviceCallSubscription.cancel();
_serviceCallSubscription = eventBus.on<ServiceCallEvent>().listen((event) {
_callService(event.domain, event.service, event.entityId, event.additionalParams);
}
if (_serviceCallSubscription == null) {
_serviceCallSubscription =
eventBus.on<ServiceCallEvent>().listen((event) {
_callService(event.domain, event.service, event.entityId,
event.additionalParams);
});
}
if (_showEntityPageSubscription != null) _showEntityPageSubscription.cancel();
_showEntityPageSubscription = eventBus.on<ShowEntityPageEvent>().listen((event) {
if (_showEntityPageSubscription == null) {
_showEntityPageSubscription =
eventBus.on<ShowEntityPageEvent>().listen((event) {
_showEntityPage(event.entity);
});
}
_refreshData() async {
setState(() {
_isLoading = true;
if (_refreshDataSubscription == null) {
_refreshDataSubscription = eventBus.on<RefreshDataEvent>().listen((event){
_refreshData();
});
}
if (_showErrorSubscription == null) {
_showErrorSubscription = eventBus.on<ShowErrorEvent>().listen((event){
_showErrorSnackBar(message: event.text, errorCode: event.errorCode);
});
}
}
_refreshData() async {
_homeAssistant.updateSettings(_webSocketApiEndpoint, _password, _authType, _useLovelaceUI);
setState(() {
_hideErrorSnackBar();
_isLoading = 1;
});
_errorCodeToBeShown = 0;
if (_homeAssistant != null) {
await _homeAssistant.fetch().then((result) {
setState(() {
//_instanceConfig = _homeAssistant.instanceConfig;
_entities = _homeAssistant.entities;
_uiViewsCount = _homeAssistant.viewsCount;
_isLoading = false;
//_uiViewsCount = _homeAssistant.viewsCount;
//TheLogger.debug("_uiViewsCount=$_uiViewsCount");
_isLoading = 0;
});
}).catchError((e) {
_setErrorState(e);
});
}
eventBus.fire(RefreshDataFinishedEvent());
}
_setErrorState(e) {
setState(() {
_errorCodeToBeShown = e["errorCode"] != null ? e["errorCode"] : 99;
_lastErrorMessage = e["errorMessage"] ?? "Unknown error";
_isLoading = false;
_isLoading = 2;
});
_showErrorSnackBar(
message: e != null ? e["errorMessage"] ?? "$e" : "Unknown error",
errorCode: e["errorCode"] != null ? e["errorCode"] : 99
);
}
void _callService(String domain, String service, String entityId, Map<String, String> additionalParams) {
setState(() {
_isLoading = true;
});
_homeAssistant.callService(domain, service, entityId, additionalParams).then((r) {
setState(() {
_isLoading = false;
});
}).catchError((e) => _setErrorState(e));
void _callService(String domain, String service, String entityId, Map<String, dynamic> additionalParams) {
_homeAssistant.callService(domain, service, entityId, additionalParams).catchError((e) => _setErrorState(e));
}
void _showEntityPage(Entity entity) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => EntityViewPage(entity: entity),
builder: (context) => EntityViewPage(entity: entity, homeAssistant: _homeAssistant),
)
);
}
List<Widget> _buildViews() {
List<Widget> result = [];
if ((_entities != null) && (!_homeAssistant.uiBuilder.isEmpty)) {
_homeAssistant.uiBuilder.views.forEach((viewId, view) {
result.add(
RefreshIndicator(
color: Colors.amber,
child: ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: _buildSingleView(view),
),
onRefresh: () => _refreshData(),
)
);
});
}
return result;
}
List<Widget> _buildSingleView(View view) {
List<Widget> result = [];
if (view.isThereBadges) {
result.add(
Wrap(
alignment: WrapAlignment.center,
spacing: 10.0,
runSpacing: 1.0,
children: _buildBadges(view.badges),
)
);
}
view.cards.forEach((id, card) {
if (card.entities.isNotEmpty) {
result.add(_buildCard(card));
}
});
return result;
}
List<Widget> _buildBadges( Map<String, Badge> badges) {
List<Widget> result = [];
badges.forEach((id, badge) {
var badgeEntity = _entities.get(id);
if (badgeEntity != null) {
result.add(
_buildSingleBadge(badgeEntity)
);
}
});
return result;
}
Widget _buildSingleBadge(Entity data) {
double iconSize = 26.0;
Widget badgeIcon;
String badgeTextValue;
Color iconColor = _badgeColors[data.domain] ?? _badgeColors["default"];
switch (data.domain) {
case "sun": {
badgeIcon = data.state == "below_horizon" ?
Icon(
MaterialDesignIcons.createIconDataFromIconCode(0xf0dc),
size: iconSize,
) :
Icon(
MaterialDesignIcons.createIconDataFromIconCode(0xf5a8),
size: iconSize,
);
break;
}
case "sensor": {
badgeTextValue = data.unitOfMeasurement;
badgeIcon = Center(
child: Text(
"${data.state == 'unknown' ? '-' : data.state}",
overflow: TextOverflow.fade,
softWrap: false,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 17.0),
),
);
break;
}
case "device_tracker": {
badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData(data, iconSize,Colors.black);
badgeTextValue = data.state;
break;
}
default: {
badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData(data, iconSize,Colors.black);
}
}
Widget badgeText;
if (badgeTextValue == null || badgeTextValue.length == 0) {
badgeText = Container(width: 0.0, height: 0.0);
} else {
badgeText = Container(
padding: EdgeInsets.fromLTRB(6.0, 2.0, 6.0, 2.0),
child: Text("$badgeTextValue",
style: TextStyle(fontSize: 12.0, color: Colors.white),
textAlign: TextAlign.center, softWrap: false, overflow: TextOverflow.fade),
decoration: new BoxDecoration(
// Circle shape
//shape: BoxShape.circle,
color: iconColor,
borderRadius: BorderRadius.circular(9.0),
)
);
}
return Column(
children: <Widget>[
Container(
margin: EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 10.0),
width: 50.0,
height: 50.0,
decoration: new BoxDecoration(
// Circle shape
shape: BoxShape.circle,
color: Colors.white,
// The border you want
border: new Border.all(
width: 2.0,
color: iconColor,
),
),
child: Stack(
overflow: Overflow.visible,
children: <Widget>[
Positioned(
width: 46.0,
height: 46.0,
top: 0.0,
left: 0.0,
child: badgeIcon,
),
Positioned(
//width: 50.0,
bottom: -9.0,
left: -10.0,
right: -10.0,
child: Center(
child: badgeText,
)
)
],
),
),
Container(
width: 60.0,
child: Text(
"${data.displayName}",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 12.0),
softWrap: true,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
),
],
);
}
Card _buildCard(HACard card) {
List<Widget> body = [];
body.add(_buildCardHeader(card.friendlyName));
body.addAll(_buildCardBody(card.entities));
Card result = Card(
child: new Column(mainAxisSize: MainAxisSize.min, children: body)
);
return result;
}
Widget _buildCardHeader(String name) {
var result;
if (name.trim().length > 0) {
result = new ListTile(
//leading: const Icon(Icons.device_hub),
//subtitle: Text(".."),
//trailing: Text("${data["state"]}"),
title: Text("$name",
textAlign: TextAlign.left,
overflow: TextOverflow.ellipsis,
style: new TextStyle(fontWeight: FontWeight.bold, fontSize: 25.0)),
);
} else {
result = new Container(width: 0.0, height: 0.0);
}
return result;
}
List<Widget> _buildCardBody(List ids) {
List<Widget> entities = [];
ids.forEach((id) {
var entity = _entities.get(id);
if (entity != null) {
entities.add(
Padding(
padding: EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 10.0),
child: entity.buildWidget(context, true),
));
}
});
return entities;
}
List<Tab> buildUIViewTabs() {
List<Tab> result = [];
if ((_entities != null) && (!_homeAssistant.uiBuilder.isEmpty)) {
_homeAssistant.uiBuilder.views.forEach((viewId, view) {
if (_homeAssistant.ui.views.isNotEmpty) {
_homeAssistant.ui.views.forEach((HAView view) {
if (view.linkedEntity == null) {
result.add(
Tab(
icon: MaterialDesignIcons.createIconWidgetFromEntityData(_entities.get(viewId), 24.0, null) ??
icon:
Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:home-assistant"),
MaterialDesignIcons.createIconDataFromIconName(
view.iconName ?? "mdi:home-assistant"),
size: 24.0,
)
)
);
} else {
result.add(
Tab(
icon: MaterialDesignIcons.createIconWidgetFromEntityData(
view.linkedEntity, 24.0, null) ??
Icon(
MaterialDesignIcons.createIconDataFromIconName(
"mdi:home-assistant"),
size: 24.0,
)
)
);
}
});
}
return result;
}
@ -445,7 +325,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
Row titleRow = Row(
children: [Text(_homeAssistant != null ? _homeAssistant.locationName : "")],
);
if (_isLoading) {
if (_isLoading == 1) {
titleRow.children.add(Padding(
child: JumpingDotsProgressIndicator(
fontSize: 26.0,
@ -453,27 +333,54 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
),
padding: const EdgeInsets.fromLTRB(5.0, 0.0, 0.0, 30.0),
));
} else if (_isLoading == 2) {
titleRow.children.add(Padding(
child: Icon(
Icons.error_outline,
size: 20.0,
color: Colors.red,
),
padding: const EdgeInsets.fromLTRB(5.0, 0.0, 0.0, 0.0),
));
}
return titleRow;
}
Drawer _buildAppDrawer() {
return new Drawer(
child: ListView(
children: <Widget>[
new UserAccountsDrawerHeader(
accountName: Text(_homeAssistant != null ? _homeAssistant.locationName : "Unknown"),
List<Widget> menuItems = [];
menuItems.add(
UserAccountsDrawerHeader(
accountName: Text(_homeAssistant.userName),
accountEmail: Text(_instanceHost ?? "Not configured"),
currentAccountPicture: new Image.asset('images/hassio-192x192.png'),
onDetailsPressed: () {
setState(() {
_accountMenuExpanded = !_accountMenuExpanded;
});
},
currentAccountPicture: CircleAvatar(
child: Text(
_homeAssistant.userAvatarText,
style: TextStyle(
fontSize: 32.0
),
new ListTile(
),
),
)
);
if (_accountMenuExpanded) {
menuItems.addAll([
ListTile(
leading: Icon(Icons.settings),
title: Text("Connection settings"),
title: Text("Settings"),
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).pushNamed('/connection-settings');
},
),
Divider(),
]);
} else {
menuItems.addAll([
new ListTile(
leading: Icon(Icons.insert_drive_file),
title: Text("Log"),
@ -490,21 +397,31 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
HAUtils.launchURL("https://github.com/estevez-dev/ha_client_pub/issues/new");
},
),
Divider(),
new AboutListTile(
applicationName: appName,
applicationVersion: appVersion,
applicationLegalese: "Keyboard Crumbs | www.keyboardcrumbs.io",
)
],
]);
}
return new Drawer(
child: ListView(
children: menuItems,
),
);
}
_checkShowInfo(BuildContext context) {
if (_errorCodeToBeShown > 0) {
String message = _lastErrorMessage;
void _hideErrorSnackBar() {
_scaffoldKey?.currentState?.hideCurrentSnackBar();
}
void _showErrorSnackBar({Key key, @required String message, @required int errorCode}) {
SnackBarAction action;
switch (_errorCodeToBeShown) {
switch (errorCode) {
case 9:
case 11:
case 7:
case 1: {
action = SnackBarAction(
label: "Retry",
@ -539,9 +456,9 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
break;
}
case 7: {
case 10: {
action = SnackBarAction(
label: "Retry",
label: "Refresh",
onPressed: () {
_scaffoldKey?.currentState?.hideCurrentSnackBar();
_refreshData();
@ -561,19 +478,14 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
break;
}
}
Timer(Duration(seconds: 1), () {
_scaffoldKey.currentState.hideCurrentSnackBar();
_scaffoldKey.currentState.showSnackBar(
SnackBar(
content: Text("$message (code: $_errorCodeToBeShown)"),
content: Text("$message (code: $errorCode)"),
action: action,
duration: Duration(hours: 1),
)
);
});
} else {
_scaffoldKey?.currentState?.hideCurrentSnackBar();
}
}
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
@ -583,7 +495,20 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
key: _scaffoldKey,
appBar: AppBar(
title: _buildAppTitle(),
bottom: empty ? null : TabBar(tabs: buildUIViewTabs()),
leading: IconButton(
icon: Icon(Icons.menu),
onPressed: () {
_scaffoldKey.currentState.openDrawer();
setState(() {
_accountMenuExpanded = false;
});
},
),
primary: true,
bottom: empty ? null : TabBar(
tabs: buildUIViewTabs(),
isScrollable: true,
),
),
drawer: _buildAppDrawer(),
body: empty ?
@ -594,27 +519,24 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:home-assistant"),
size: 100.0,
color: _errorCodeToBeShown == 0 ? Colors.blue : Colors.redAccent,
color: _isLoading == 2 ? Colors.redAccent : Colors.blue,
),
]
),
)
:
TabBarView(
children: _buildViews()
),
_homeAssistant.buildViews(context, _useLovelaceUI)
);
}
@override
Widget build(BuildContext context) {
_checkShowInfo(context);
// This method is rerun every time setState is called.
if (_entities == null) {
if (_homeAssistant.entities.isEmpty) {
return _buildScaffold(true);
} else {
return DefaultTabController(
length: _uiViewsCount,
length: _homeAssistant.ui.views.length,
child: _buildScaffold(false)
);
}
@ -627,7 +549,9 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
if (_settingsSubscription != null) _settingsSubscription.cancel();
if (_serviceCallSubscription != null) _serviceCallSubscription.cancel();
if (_showEntityPageSubscription != null) _showEntityPageSubscription.cancel();
_homeAssistant.closeConnection();
if (_refreshDataSubscription != null) _refreshDataSubscription.cancel();
if (_showErrorSubscription != null) _showErrorSubscription.cancel();
_homeAssistant.disconnect();
super.dispose();
}
}

View File

@ -16,7 +16,12 @@ class MaterialDesignIcons {
"input_text": "mdi:textbox",
"sun": "mdi:white-balance-sunny",
"scene": "mdi:google-pages",
"media_player": "mdi:cast"
"media_player": "mdi:cast",
"climate": "mdi:thermostat",
"cover.open": "mdi:window-open",
"cover.closed": "mdi:window-closed",
"cover.closing": "mdi:window-open",
"cover.opening": "mdi:window-open",
};
static Map _defaultIconsByDeviceClass = {
@ -68,7 +73,14 @@ class MaterialDesignIcons {
//"sensor.illuminance": "mdi:",
"sensor.temperature": "mdi:thermometer",
//"cover.window": "mdi:",
//"cover.garage": "mdi:",
"cover.garage.closed": "mdi:garage",
"cover.garage.open": "mdi:garage-open",
"cover.garage.opening": "mdi:garage-open",
"cover.garage.closing": "mdi:garage-open",
"cover.window.open": "mdi:window-open",
"cover.window.closed": "mdi:window-closed",
"cover.window.closing": "mdi:window-open",
"cover.window.opening": "mdi:window-open",
};
static Map _iconsDataMap = {
"mdi:access-point": 0xf002,
@ -2915,7 +2927,7 @@ class MaterialDesignIcons {
static int getDefaultIconByEntityId(String entityId, String deviceClass, String state) {
String domain = entityId.split(".")[0];
String iconNameByDomain = _defaultIconsByDomains[domain];
String iconNameByDomain = _defaultIconsByDomains["$domain.$state"] ?? _defaultIconsByDomains["$domain"];
String iconNameByDeviceClass;
if (deviceClass != null) {
iconNameByDeviceClass = _defaultIconsByDeviceClass["$domain.$deviceClass.$state"] ?? _defaultIconsByDeviceClass["$domain.$deviceClass"];

View File

@ -11,14 +11,31 @@ class ConnectionSettingsPage extends StatefulWidget {
class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
String _hassioDomain = "";
String _hassioPort = "8123";
String _newHassioDomain = "";
String _hassioPort = "";
String _newHassioPort = "";
String _hassioPassword = "";
String _newHassioPassword = "";
String _socketProtocol = "wss";
String _newSocketProtocol = "wss";
String _authType = "access_token";
String _newAuthType = "access_token";
bool _useLovelace = false;
bool _newUseLovelace = false;
bool _edited = false;
FocusNode _domainFocusNode;
FocusNode _portFocusNode;
FocusNode _passwordFocusNode;
@override
void initState() {
super.initState();
_domainFocusNode = FocusNode();
_portFocusNode = FocusNode();
_passwordFocusNode = FocusNode();
_domainFocusNode.addListener(_checkConfigChanged);
_portFocusNode.addListener(_checkConfigChanged);
_passwordFocusNode.addListener(_checkConfigChanged);
_loadSettings();
}
@ -26,25 +43,42 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
SharedPreferences prefs = await SharedPreferences.getInstance();
setState(() {
_hassioDomain = prefs.getString("hassio-domain");
_hassioPort = prefs.getString("hassio-port") ?? '8123';
_hassioPassword = prefs.getString("hassio-password");
_socketProtocol = prefs.getString("hassio-protocol") ?? 'wss';
_authType = prefs.getString("hassio-auth-type") ?? 'access_token';
_hassioDomain = _newHassioDomain = prefs.getString("hassio-domain")?? "";
_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;
} catch (e) {
_useLovelace = _newUseLovelace = false;
}
});
}
void _checkConfigChanged() {
setState(() {
_edited = ((_newHassioPassword != _hassioPassword) ||
(_newHassioPort != _hassioPort) ||
(_newHassioDomain != _hassioDomain) ||
(_newSocketProtocol != _socketProtocol) ||
(_newAuthType != _authType) ||
(_newUseLovelace != _useLovelace));
});
}
_saveSettings() async {
if (_hassioDomain.indexOf("http") == 0 && _hassioDomain.indexOf("//") > 0) {
_hassioDomain = _hassioDomain.split("//")[1];
if (_newHassioDomain.indexOf("http") == 0 && _newHassioDomain.indexOf("//") > 0) {
_newHassioDomain = _newHassioDomain.split("//")[1];
}
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setString("hassio-domain", _hassioDomain);
prefs.setString("hassio-port", _hassioPort);
prefs.setString("hassio-password", _hassioPassword);
prefs.setString("hassio-protocol", _socketProtocol);
prefs.setString("hassio-res-protocol", _socketProtocol == "wss" ? "https" : "http");
prefs.setString("hassio-auth-type", _authType);
prefs.setString("hassio-domain", _newHassioDomain);
prefs.setString("hassio-port", _newHassioPort);
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);
}
@override
@ -52,26 +86,39 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
return new Scaffold(
appBar: new AppBar(
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
_saveSettings().then((r){
Navigator.pop(context);
});
eventBus.fire(SettingsChangedEvent(true));
}),
title: new Text(widget.title),
actions: <Widget>[
IconButton(
icon: Icon(Icons.check),
onPressed: _edited ? (){
_saveSettings().then((r){
Navigator.pop(context);
eventBus.fire(SettingsChangedEvent(true));
});
} : null
)
],
),
body: ListView(
padding: const EdgeInsets.all(20.0),
children: <Widget>[
Text(
"Connection settings",
style: TextStyle(
color: Colors.black45,
fontSize: 20.0
),
),
new Row(
children: [
Text("Use ssl (HTTPS)"),
Switch(
value: (_socketProtocol == "wss"),
value: (_newSocketProtocol == "wss"),
onChanged: (value) {
setState(() {
_socketProtocol = value ? "wss" : "ws";
});
_saveSettings();
_newSocketProtocol = value ? "wss" : "ws";
_checkConfigChanged();
},
)
],
@ -80,36 +127,46 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
decoration: InputDecoration(
labelText: "Home Assistant domain or ip address"
),
controller: TextEditingController(
text: _hassioDomain
controller: new TextEditingController.fromValue(
new TextEditingValue(
text: _newHassioDomain,
selection:
new TextSelection.collapsed(offset: _newHassioDomain.length)
)
),
onChanged: (value) {
_hassioDomain = value;
_saveSettings();
_newHassioDomain = value;
},
focusNode: _domainFocusNode,
onEditingComplete: _checkConfigChanged,
),
new TextField(
decoration: InputDecoration(
labelText: "Home Assistant port"
labelText: "Home Assistant port (default is 8123)"
),
controller: TextEditingController(
text: _hassioPort
controller: new TextEditingController.fromValue(
new TextEditingValue(
text: _newHassioPort,
selection:
new TextSelection.collapsed(offset: _newHassioPort.length)
)
),
onChanged: (value) {
_hassioPort = value;
_saveSettings();
_newHassioPort = value;
//_saveSettings();
},
focusNode: _portFocusNode,
onEditingComplete: _checkConfigChanged,
),
new Row(
children: [
Text("Login with access token (HA >= 0.78.0)"),
Switch(
value: (_authType == "access_token"),
value: (_newAuthType == "access_token"),
onChanged: (value) {
setState(() {
_authType = value ? "access_token" : "api_password";
});
_saveSettings();
_newAuthType = value ? "access_token" : "api_password";
_checkConfigChanged();
//_saveSettings();
},
)
],
@ -118,16 +175,55 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
decoration: InputDecoration(
labelText: _authType == "access_token" ? "Access token" : "API password"
),
controller: TextEditingController(
text: _hassioPassword
controller: new TextEditingController.fromValue(
new TextEditingValue(
text: _newHassioPassword,
selection:
new TextSelection.collapsed(offset: _newHassioPassword.length)
)
),
onChanged: (value) {
_hassioPassword = value;
_saveSettings();
_newHassioPassword = value;
//_saveSettings();
},
focusNode: _passwordFocusNode,
onEditingComplete: _checkConfigChanged,
),
Padding(
padding: EdgeInsets.only(top: 20.0),
child: Text(
"UI",
style: TextStyle(
color: Colors.black45,
fontSize: 20.0
),
),
),
new Row(
children: [
Text("Use Lovelace UI"),
Switch(
value: _newUseLovelace,
onChanged: (value) {
_newUseLovelace = value;
_checkConfigChanged();
},
)
],
),
],
),
);
}
@override
void dispose() {
_domainFocusNode.removeListener(_checkConfigChanged);
_portFocusNode.removeListener(_checkConfigChanged);
_passwordFocusNode.removeListener(_checkConfigChanged);
_domainFocusNode.dispose();
_portFocusNode.dispose();
_passwordFocusNode.dispose();
super.dispose();
}
}

View File

@ -1,66 +0,0 @@
part of 'main.dart';
class UIBuilder {
EntityCollection _entities;
Map<String, View> _views;
static List badgeDomains = ["alarm_control_panel", "binary_sensor", "device_tracker", "updater", "sun", "timer", "sensor"];
bool get isEmpty => _views.length == 0;
Map<String, View> get views => _views ?? {};
UIBuilder() {
_views = {};
}
static bool isBadge(String domain) {
return badgeDomains.contains(domain);
}
void build(EntityCollection entitiesCollection) {
_entities = entitiesCollection;
_views.clear();
if (!_entities.hasDefaultView) {
_createDefaultView();
}
_createViews(entitiesCollection.viewList);
}
void _createDefaultView() {
Map<String, List<String>> userGroupsList = _entities.getDefaultViewTopLevelEntities();
TheLogger.log("RESULT", "${userGroupsList["userGroups"]}");
TheLogger.log("RESULT", "${userGroupsList["notGroupedEntities"]}");
View view = View("group.default_view", 0);
userGroupsList["userGroups"].forEach((groupId){
view.add(_entities.get(groupId));
});
userGroupsList["notGroupedEntities"].forEach((entityId){
view.add(_entities.get(entityId));
});
_views["group.default_view"] = view;
}
void _createViews(List<String> viewsList) {
int counter = 0;
viewsList.forEach((viewId) {
counter += 1;
View view = View(viewId, counter);
try {
Entity viewGroupEntity = _entities.get(viewId);
viewGroupEntity.childEntities.forEach((
entityId) { //Each entity or group in view
if (_entities.isExist(entityId)) {
view.add(_entities.get(entityId));
} else {
TheLogger.log("Warning", "Unknown entity inside view: $entityId");
}
});
} catch (error) {
TheLogger.log("Error","Error parsing view: $viewId");
}
_views[viewId] = view;
});
}
}

View File

@ -0,0 +1,60 @@
part of '../main.dart';
class HACard {
List<Entity> entities = [];
Entity linkedEntity;
String name;
String id;
String type;
HACard({
this.name,
this.id,
this.linkedEntity,
@required this.type
});
Widget build(BuildContext context) {
switch (type) {
case "entities": {
return EntitiesCardWidget(
card: this,
);
}
case "weather-forecast":
case "thermostat":
case "sensor":
case "plant-status":
case "picture-entity":
case "picture-elements":
case "picture":
case "map":
case "iframe":
case "gauge":
case "entity-button":
case "conditional":
case "alarm-panel":
case "media-control": {
return UnsupportedCardWidget(
card: this,
);
}
default: {
if ((linkedEntity == null) && (entities.isNotEmpty)) {
return EntitiesCardWidget(
card: this,
);
} else {
return UnsupportedCardWidget(
card: this,
);
}
}
}
}
}

26
lib/ui_class/ui.dart Normal file
View File

@ -0,0 +1,26 @@
part of '../main.dart';
class HomeAssistantUI {
List<HAView> views;
HomeAssistantUI() {
views = [];
}
Widget build(BuildContext context) {
return TabBarView(
children: _buildViews(context)
);
}
List<Widget> _buildViews(BuildContext context) {
List<Widget> result = [];
views.forEach((view) {
result.add(
view.build(context)
);
});
return result;
}
}

View File

@ -0,0 +1,66 @@
part of '../main.dart';
class HAView {
List<HACard> cards = [];
List<Entity> badges = [];
Entity linkedEntity;
String name;
String id;
String iconName;
int count;
HAView({
this.name,
this.id,
this.count,
this.iconName,
List<Entity> childEntities
}) {
if (childEntities != null) {
_fillView(childEntities);
}
}
void _fillView(List<Entity> childEntities) {
List<HACard> autoGeneratedCards = [];
childEntities.forEach((entity) {
if (entity.isBadge) {
badges.add(entity);
TheLogger.debug("----Badge: ${entity.entityId}");
} else {
if (!entity.isGroup) {
String groupIdToAdd = "${entity.domain}.${entity.domain}$count";
if (autoGeneratedCards.every((HACard card) => card.id != groupIdToAdd )) {
HACard card = HACard(
id: groupIdToAdd,
name: entity.domain,
type: "entities"
);
TheLogger.debug("----Creating card: $groupIdToAdd");
card.entities.add(entity);
autoGeneratedCards.add(card);
} else {
autoGeneratedCards.firstWhere((card) => card.id == groupIdToAdd).entities.add(entity);
}
} else {
TheLogger.debug("----Card: ${entity.entityId}");
HACard card = HACard(
name: entity.displayName,
id: entity.entityId,
linkedEntity: entity,
type: "entities"
);
card.entities.addAll(entity.childEntities);
cards.add(card);
}
}
});
cards.addAll(autoGeneratedCards);
}
Widget build(BuildContext context) {
return ViewWidget(
view: this,
);
}
}

View File

@ -0,0 +1,25 @@
part of '../main.dart';
class CardHeaderWidget extends StatelessWidget {
final String name;
const CardHeaderWidget({Key key, this.name}) : super(key: key);
@override
Widget build(BuildContext context) {
var result;
if ((name != null) && (name.trim().length > 0)) {
result = new ListTile(
title: Text("$name",
textAlign: TextAlign.left,
overflow: TextOverflow.ellipsis,
style: new TextStyle(fontWeight: FontWeight.bold, fontSize: 25.0)),
);
} else {
result = new Container(width: 0.0, height: 0.0);
}
return result;
}
}

View File

@ -0,0 +1,39 @@
part of '../main.dart';
class EntitiesCardWidget extends StatelessWidget {
final HACard card;
const EntitiesCardWidget({
Key key,
this.card
}) : super(key: key);
@override
Widget build(BuildContext context) {
if ((card.linkedEntity!= null) && (card.linkedEntity.isHidden)) {
return Container(width: 0.0, height: 0.0,);
}
List<Widget> body = [];
body.add(CardHeaderWidget(name: card.name));
body.addAll(_buildCardBody(context));
return Card(
child: new Column(mainAxisSize: MainAxisSize.min, children: body)
);
}
List<Widget> _buildCardBody(BuildContext context) {
List<Widget> result = [];
card.entities.forEach((Entity entity) {
if (!entity.isHidden) {
result.add(
Padding(
padding: EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 10.0),
child: entity.buildDefaultWidget(context),
));
}
});
return result;
}
}

View File

@ -0,0 +1,27 @@
part of '../main.dart';
class MediaControlCardWidget extends StatelessWidget {
final HACard card;
const MediaControlCardWidget({
Key key,
this.card
}) : super(key: key);
@override
Widget build(BuildContext context) {
if ((card.linkedEntity!= null) && (card.linkedEntity.isHidden)) {
return Container(width: 0.0, height: 0.0,);
}
List<Widget> body = [];
return Card(
child: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: body
)
);
}
}

View File

@ -0,0 +1,49 @@
part of '../main.dart';
class UnsupportedCardWidget extends StatelessWidget {
final HACard card;
const UnsupportedCardWidget({
Key key,
this.card
}) : super(key: key);
@override
Widget build(BuildContext context) {
if ((card.linkedEntity!= null) && (card.linkedEntity.isHidden)) {
return Container(width: 0.0, height: 0.0,);
}
List<Widget> body = [];
body.add(CardHeaderWidget(name: card.name ?? ""));
body.addAll(_buildCardBody(context));
return Card(
child: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: body
)
);
}
List<Widget> _buildCardBody(BuildContext context) {
List<Widget> result = [];
if (card.linkedEntity != null) {
result.addAll(<Widget>[
Padding(
padding: EdgeInsets.fromLTRB(0.0, Entity.rowPadding, 0.0, Entity.rowPadding),
child: card.linkedEntity.buildDefaultWidget(context),
)
]);
} else {
result.addAll(<Widget>[
Padding(
padding: EdgeInsets.fromLTRB(Entity.leftWidgetPadding, Entity.rowPadding, Entity.rightWidgetPadding, Entity.rowPadding),
child: Text("'${card.type}' card is not supported yet"),
),
]);
}
return result;
}
}

95
lib/ui_widgets/view.dart Normal file
View File

@ -0,0 +1,95 @@
part of '../main.dart';
class ViewWidget extends StatefulWidget {
final HAView view;
const ViewWidget({
Key key,
this.view
}) : super(key: key);
@override
State<StatefulWidget> createState() {
return ViewWidgetState();
}
}
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(
physics: const AlwaysScrollableScrollPhysics(),
children: _buildChildren(context),
),
onRefresh: () => _refreshData(),
);
}
List<Widget> _buildChildren(BuildContext context) {
List<Widget> result = [];
if (widget.view.badges.isNotEmpty) {
result.insert(0,
Wrap(
alignment: WrapAlignment.center,
spacing: 10.0,
runSpacing: 1.0,
children: _buildBadges(context),
)
);
}
widget.view.cards.forEach((HACard card){
result.add(
card.build(context)
);
});
return result;
}
List<Widget> _buildBadges(BuildContext context) {
List<Widget> result = [];
widget.view.badges.forEach((Entity entity) {
if (!entity.isHidden) {
result.add(entity.buildBadgeWidget(context));
}
});
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

@ -20,12 +20,25 @@ class TheLogger {
return inDebugMode;
}
static void log(String level, String message) {
static void error(String message) {
_writeToLog("Error", message);
}
static void warning(String message) {
_writeToLog("Warning", message);
}
static void debug(String message) {
_writeToLog("Debug", message);
}
static void _writeToLog(String level, String message) {
if (isInDebugMode) {
debugPrint('$message');
}
_log.add("[$level] : $message");
if (_log.length > 50) {
DateTime t = DateTime.now();
_log.add("${formatDate(t, ["mm","dd"," ","HH",":","nn",":","ss"])} [$level] : $message");
if (_log.length > 100) {
_log.removeAt(0);
}
}
@ -37,7 +50,7 @@ class HAUtils {
if (await canLaunch(url)) {
await launch(url);
} else {
TheLogger.log("Error", "Could not launch $url");
TheLogger.error( "Could not launch $url");
}
}
}
@ -56,11 +69,19 @@ class SettingsChangedEvent {
SettingsChangedEvent(this.reconnect);
}
class RefreshDataEvent {
RefreshDataEvent();
}
class RefreshDataFinishedEvent {
RefreshDataFinishedEvent();
}
class ServiceCallEvent {
String domain;
String service;
String entityId;
Map<String, String> additionalParams;
Map<String, dynamic> additionalParams;
ServiceCallEvent(this.domain, this.service, this.entityId, this.additionalParams);
}
@ -70,3 +91,10 @@ class ShowEntityPageEvent {
ShowEntityPageEvent(this.entity);
}
class ShowErrorEvent {
String text;
int errorCode;
ShowErrorEvent(this.text, this.errorCode);
}

View File

@ -1,53 +0,0 @@
part of 'main.dart';
class View {
String _entityId;
int _count;
Map<String, HACard> cards;
Map<String, Badge> badges;
bool get isThereBadges => (badges != null) && (badges.isNotEmpty);
View(String groupId, int viewCount) {
_entityId = groupId;
_count = viewCount;
cards = {};
badges = {};
}
void add(Entity entity) {
if (!entity.isGroup) {
_addEntityWithoutGroup(entity);
} else {
_addCardWithEntities(entity);
}
}
void _addBadge(String entityId) {
badges.addAll({entityId: Badge(entityId)});
}
void _addEntityWithoutGroup(Entity entity) {
if (UIBuilder.isBadge(entity.domain)) {
//This is badge
_addBadge(entity.entityId);
} else {
//This is a standalone entity
String groupIdToAdd = "${entity.domain}.${entity.domain}$_count";
if (cards[groupIdToAdd] == null) {
_addCard(groupIdToAdd, entity.domain);
}
cards[groupIdToAdd].addEntity(entity.entityId);
}
}
void _addCard(String entityId, String friendlyName) {
cards.addAll({"$entityId": HACard(entityId, friendlyName)});
}
void _addCardWithEntities(Entity entity) {
cards.addAll({"${entity.entityId}": HACard(entity.entityId, entity.displayName)});
cards[entity.entityId].addEntities(entity.childEntities);
}
}

View File

@ -50,6 +50,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.2"
charts_common:
dependency: transitive
description:
name: charts_common
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.0"
charts_flutter:
dependency: "direct main"
description:
name: charts_flutter
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.0"
collection:
dependency: transitive
description:
@ -113,8 +127,15 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.2"
flutter_launcher_icons:
flutter_colorpicker:
dependency: "direct main"
description:
name: flutter_colorpicker
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.0"
flutter_launcher_icons:
dependency: "direct dev"
description:
name: flutter_launcher_icons
url: "https://pub.dartlang.org"
@ -174,6 +195,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.4"
intl:
dependency: transitive
description:
name: intl
url: "https://pub.dartlang.org"
source: hosted
version: "0.15.7"
io:
dependency: transitive
description:
@ -251,13 +279,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.5"
package_info:
dependency: "direct main"
description:
name: package_info
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.2"
package_resolver:
dependency: transitive
description:
@ -327,7 +348,7 @@ packages:
name: shared_preferences
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.2"
version: "0.4.3"
shelf:
dependency: transitive
description:
@ -495,5 +516,5 @@ packages:
source: hosted
version: "2.1.15"
sdks:
dart: ">=2.0.0 <=2.1.0-dev.3.1.flutter-760a9690c2"
dart: ">=2.0.0 <=2.1.0-dev.5.0.flutter-a2eb050044"
flutter: ">=0.1.4 <2.0.0"

View File

@ -1,7 +1,7 @@
name: hass_client
description: Home Assistant Android Client
version: 0.2.2+24
version: 0.3.4+52
environment:
sdk: ">=2.0.0-dev.68.0 <3.0.0"
@ -12,11 +12,11 @@ dependencies:
shared_preferences: any
progress_indicators: ^0.1.2
event_bus: ^1.0.1
package_info: ^0.3.2
flutter_launcher_icons: ^0.6.1
cached_network_image: ^0.4.1
url_launcher: ^3.0.3
date_format: ^1.0.5
flutter_colorpicker: ^0.1.0
charts_flutter: ^0.4.0
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
@ -25,6 +25,7 @@ dependencies:
dev_dependencies:
flutter_test:
sdk: flutter
flutter_launcher_icons: ^0.6.1
flutter_icons:
android: true