diff --git a/code/examples/cbus.mht b/code/examples/cbus.mht new file mode 100644 index 000000000..4f5aae93d --- /dev/null +++ b/code/examples/cbus.mht @@ -0,0 +1,13 @@ +# This file is an example MHT file for the Clipsal CBus module, providing +# just a definition of the CGate interface. When MisterHouse runs with a valid +# CGate definition, but no other CBus objects defined, it will query the +# CBus network for known objects and units, and write a corresponding +# MHT file including their definitions. + +Format = A + +#TYPE, Address, Name, GroupList, Other + +CBUS_CGATE, CGATE + + diff --git a/code/examples/cbus.mht.example b/code/examples/cbus.mht.example new file mode 100644 index 000000000..4e6342531 --- /dev/null +++ b/code/examples/cbus.mht.example @@ -0,0 +1,41 @@ +# This file is an example MHT file for the Clipsal CBus module. +# Note that you should generate your own by defining only the +# CGate object, and letting MisterHouse generate the others. This +# example is provided purely to show the format. + +Format = A + +#TYPE, Address, Name, GroupList, Other + +CBUS_CGATE, CGATE + +# CBus trigger group addresses. + +CBUS_TRIGGER, //HOME/254/202/1, Kitchen_Trigger_Group, All_CBus, Kitchen Trigger Group +CBUS_TRIGGER, //HOME/254/202/2, Bed_2_Fan_Speed, All_CBus|Bed_2, Bed 2 Fan Speed +CBUS_TRIGGER, //HOME/254/202/4, Bed_3_Fan_Speed, All_CBus|Bed_3, Bed 3 Fan Speed +CBUS_TRIGGER, //HOME/254/202/5, Master_Bed_Fan_Speed, All_CBus|Master_Bed, Master Bed Fan Speed + +# CBus "lighting" group addresses. + +CBUS_GROUP, //HOME/254/56/0, Study_Light, All_CBus|Study, Study Light +CBUS_GROUP, //HOME/254/56/1, Front_Entrance_Light, All_CBus|Lounge|Stairway, Front Entrance Light +CBUS_GROUP, //HOME/254/56/2, Outside_Front_Lights, All_CBus|Outside, Outside Front Lights +CBUS_GROUP, //HOME/254/56/3, Kitchen_Downlights, All_CBus|Kitchen, Kitchen Downlights +CBUS_GROUP, //HOME/254/56/4, Master_Bed_Fan, All_CBus|Master_Bed, Master Bed Fan +CBUS_GROUP, //HOME/254/56/5, Downstairs_Hallway_Spotlight, All_CBus|Kitchen|Lounge, Downstairs Hallway Spotlight +CBUS_GROUP, //HOME/254/56/6, Downstairs_Bathroom_Heatlamp_1, All_CBus|Downstairs_Bathroom, Downstairs Bathroom Heatlamp 1 +CBUS_GROUP, //HOME/254/56/7, Downstairs_Bathroom_Heatlamp_2, All_CBus|Downstairs_Bathroom, Downstairs Bathroom Heatlamp 2 +CBUS_GROUP, //HOME/254/56/8, Downstairs_Bathroom_Fan, All_CBus|Downstairs_Bathroom, Downstairs Bathroom Fan +CBUS_GROUP, //HOME/254/56/9, Guest_Bedroom_Cupboard_Light, All_CBus|Guest_Bedroom, Guest Bedroom Cupboard Light + +# CBus Unit addresses. + +CBUS_UNIT, 0, USB_PC_Interface, +CBUS_UNIT, 3, Downstairs_Relay_1, +CBUS_UNIT, 4, Upstairs_Relay_1, +CBUS_UNIT, 5, Upstairs_Dimmer_1, +CBUS_UNIT, 6, Upstairs_Hallway_Sensor, +CBUS_UNIT, 7, Bed_3_Switch, +CBUS_UNIT, 8, Bed_2_Switch, +CBUS_UNIT, 9, Master_Bed_Switch, diff --git a/code/public/cbus.pl b/code/public/cbus.pl index 5153c651c..0afcd1c40 100755 --- a/code/public/cbus.pl +++ b/code/public/cbus.pl @@ -7,6 +7,9 @@ # Copyright 2002: Richard Morgan omegaATbigpondDOTnetDOT.au # Copyright 2008: Andrew McCallum, Mandoon Technologies andyATmandoonDOTcomDOTau # +# IMPORTANT NOTICE - Clipsal CBus support is now provided by Clipsal_CBus.pm +# +# # $Id$ # # Ensure XML::Simple is installed: @@ -98,6 +101,10 @@ # V3.0.3 2013-11-28 # Test debug flag for logging statements. # +# V4.0 2016-03-25 +# Refactor cbus.pl to make CBus support more MisterHouse "native". +# See lib/Clipsal_CBus.pm for more information. +# # How Cgate integrates with MH # # All Cbus objects are defined in a standard XML file (cbus.xml), this file is @@ -175,6 +182,8 @@ use XML::Simple qw(:strict); +print_log "IMPORTANT NOTICE - Clipsal CBus support is now provided by Clipsal_CBus.pm" + # # Define Defaults # diff --git a/lib/Clipsal_CBus.pm b/lib/Clipsal_CBus.pm new file mode 100644 index 000000000..305d4e329 --- /dev/null +++ b/lib/Clipsal_CBus.pm @@ -0,0 +1,295 @@ + +=head1 B + +=head2 SYNOPSIS + +Clipsal_CBus.pm - support for Clipsal CBus. + +=head2 DESCRIPTION + +This module adds support for Clipsal CBus automation systems, and is a refactor of the original cbus.pl code by Richard Morgan and Andrew McCallum. + +******* IMPORTANT ******* +*** +*** You must change the C-Gate configuration files found under the 'config' directory. +*** +*** In C-GateConfig.txt: Change global-event-level from 5 to 7, the new line will be: +*** "global-event-level=7" +*** +*** In access.txt: Add a new line with your subnet, +*** eg. if your IP address is 192.168.72.42, then add the following line: +*** "interface 192.168.72.255 Program" +*** +******* IMPORTANT ******* + +=head3 How CGate integrates with MH + +All CBus objects (i.e. the CGate interface, groups and units) are defined in a standard mht +file. Misterhouse creates objects from the mht file at $Reload, and subsequently creates corresponding voice command +objects on the next $Reload. As the MH obects are created, their details (e.g. address, name) are added +to a hash oh hashes (one each for Groups and Units) which is used to map a received CBus message back to an MH object. + +On $Relaod, this module checks to see if there is a hash of group objects. If not, presumablt we haven't defined any group objects in +mht file, so we walk the tree of objects in CBus, and output to results to a generated mht file. So, all that's needed to get going is +to: + +1) Create an mht file with just a CGate object defined. There is an example in the code directory. +2) Define the CBus settings in the ini file (see below) +3) Run MH to generate an mht file (typically called cbus.mht.generated) +4) Rename the generated mht file to make it valid (e.g. cbus.mht) +5) Edit the mht file as required, e.g. to add group objects to MH groups +6) reload +7) Enjoy (and report bugs) + +The $object_vs are all voice commands, and in this version they are NOT used to control +a Cbus device from the web. Each CBus group object has it's own set() method which +ensures that any actions are reflected both in MH and on the CBus. Therefore, any changes to CBus are reflected +in the web interface in real time. + +Each $v_object is tied to its respective $object. In program control (testing the state of an $object, or setting a $object) are all +performed against the $object, although you can set the $v_object, its state will not +reflect any updates from the actual CBus. + +Remember, the CBus is interactive, it can receive as well as issue commands. + +So, you should always use the $object in your code, as it has it's own set() method. Notice how the last 'set_by' +directive was also passed, this is to ensure that we do not create endless message loops. +When the set() sub is called the actual CBus device is set to that state +assuming it was not the CBus that actually initiated this set in the first place. For example, in user code, +you might use something like this:- + + if (time_now "$Time_Sunset") { + speak "I just turned the entry light on at $Time_Now"; + $Front_Entrance_Light->set('on','user code'); + } + +CGate itself repeats all commands received back to MH via the CBus monitor. Therefore MH listens for these +commands and then sets the appropriate $object, but this is ignored if MH was in fact the source of the set. + +When MH starts up, the cbus code will automatically attempt to sync MH to the current +state of CGate. CGate of course, will reflect the physical state of the CBus network. +When the sync is complete, the $CBus_Sync will be set ON. + +mh.private.ini Settings +=============== +Category = CBus +cbus_project_name = CARLYLE +cgate_mon_address = 192.168.1.180:20024 +cgate_talk_address = 192.168.1.180:20023 +cbus_dat_file = cbus.xml #deprecated from original cbus.pl code +cbus_mht_file = cbus.mht.generated +cbus_category_prefix = cbus_ #deprecated from original cbus.pl code +cbus_ramp_speed = 0 + +=cut + +package Clipsal_CBus; + +use strict; + +%Clipsal_CBus::Groups = (); +%Clipsal_CBus::Units = (); +$Clipsal_CBus::Command_Counter = 0; +$Clipsal_CBus::Command_Counter_Max = 100; + +$Clipsal_CBus::Talker = + new Socket_Item( undef, undef, $::config_parms{cgate_talk_address} ); +$Clipsal_CBus::Monitor = + new Socket_Item( undef, undef, $::config_parms{cgate_mon_address} ); + +$Clipsal_CBus::Talker_last_sent = "N/A"; + +=head2 FUNCTIONS + +=over + +=item C + +Provides a standard logging function for the CBus packages. + +=cut + +#log levels +my $warn = 1; +my $notice = 2; +my $info = 3; +my $debug = 4; +my $trace = 5; + +&::print_log("[Clipsal CBus] CBus logging at level $::Debug{cbus}"); + +sub debug { + my ( $self, $message, $level ) = @_; + $level = $info if $level eq ''; + my $line = ''; + my @caller = caller(0); + if ( $::Debug{cbus} >= $level || $level == 0 ) { + $line = " at line " . $caller[2] + if $::Debug{cbus} >= $trace; + &::print_log( "[" . $caller[0] . "] " . $message . $line ); + } +} + +=item C + +Generates voice commands correspnding to the CBus group objects. When a new CGate object is instantiated, it +adds a post reload hook into &main to run this function. + +=cut + +sub generate_voice_commands { + + &::print_log( + "[Clipsal CBus] Generating Voice commands for all CBus group objects"); + + my $object_string; + for my $object (&main::list_all_objects) { + next unless ref $object; + next unless $object->isa('Clipsal_CBus::Group'); + + #get object name to use as part of variable in voice command + my $object_name = $object->get_object_name; + my $object_name_v = $object_name . '_v'; + $object_string .= "use vars '${object_name}_v';\n"; + my $command = $object->{label}; + + #Get list of all voice commands from the object + my $voice_cmds = $object->get_voice_cmds(); + + #Initialize the voice command with all of the possible device commands + $object_string .= "$object_name_v = new Voice_Cmd '$command [" + . join( ",", sort keys %$voice_cmds ) . "]';\n"; + + #Tie the proper routine to each voice command + foreach ( keys %$voice_cmds ) { + $object_string .= + "$object_name_v -> tie_event('" + . $voice_cmds->{$_} + . "', '$_');\n\n"; + } + + #Add this object to the list of CBus Voice Commands on the Web Interface + $object_string .= ::store_object_data( + $object_name_v, 'Voice_Cmd', + 'Clipsal CBus', 'Clipsal_CBus_commands' + ); + } + + #Evaluate the resulting object generating string + package main; + + eval $object_string; + print_log("Error in cbus_item_commands: $@\n") if $@; + + use vars '$CBus_Talker_v'; + $CBus_Talker_v = new Voice_Cmd("cbus talker [Status,Scan]"); + &main::register_object_by_name( '$CBus_Talker_v', $CBus_Talker_v ); + $CBus_Talker_v->{category} = "Clipsal CBus"; + $CBus_Talker_v->{filename} = "Clipsal_CBus_commands"; + $CBus_Talker_v->{object_name} = '$CBus_Talker_v'; + + use vars '$CBus_Monitor_v'; + $CBus_Monitor_v = new Voice_Cmd("cbus monitor [Status]"); + &main::register_object_by_name( '$CBus_Monitor_v', $CBus_Monitor_v ); + $CBus_Monitor_v->{category} = "Clipsal CBus"; + $CBus_Monitor_v->{filename} = "Clipsal_CBus_commands"; + $CBus_Monitor_v->{object_name} = '$CBus_Monitor_v'; + + package Clipsal_CBus; +} + +=head1 AUTHOR + +Richard Morgan, omegaATbigpondDOTnetDOTau +Andrew McCallum, Mandoon Technologies, andyATmandoonDOTcomDOTau +Jon Whitear, jonATwhitearDOTorg + +=head1 VERSION HOSTORY + +03-12-2001 + Modified to support c-gate 1.5 +23-06-2002 + Monitor: Source name now works, and shows 'MH' is source 0 +05-07-2002 + Modified for cbus_dat.csv input file support + Added groups and set_info support +06-07-2002 + Minor changes to support new cbus_builder + Modified to support global %cbus_data hash + removed make_cbus_file(), replaced with cbus_builder.pl +11-07-2002 + Added announce flag to cbus_dat.csv, and conditional speak flag $announce +19-09-2002 + Fixed bug in cbus_set() that prevented dimming numeric % set values + being accepted. Dimming now works. +21-09-2002 + Modified cbus_groups and cbus_catagories to read from input file + rather than hard coded + Put in config item cbus_category_prefix + Comments in input file now allowed + Fixed some other minor things +22-09-2002 V2.0 + Collapsed cbus_talker.pl, cbus_builder.pl and cbus_monitor.pl + into one new file, cbus.pl. Now issued as V2.0. + +V2.1 Fixed up some menu uglies. + Improved coding in monitor loop + Fixed up code labels, docs etc + +V2.2 Changed all speak() calls to say 'C-Bus' rather than 'CBus', so the diction is correct + +V2.2.1 Fixed minor bug in cbus monitor start voice command + +V2.2.2 Implemented; + oneshot device type + cbus_oneshot_log config param + +V2.2.3 Made the dump_cbus_data format pretty HTML tables + +V3.0 2008-02-04 + Fixed to work with C-Gate Version: v2.6.1 (build 2236) + Latest version as of June 2008 + Now reports the name of the source unit that modified a group level. + Added ability to scan CGate for groups and output to config file. + *** Configuration only requires running Builder to scan cgate and + *** build XML file, then commanding MH to "reload code". Job Done. + *** Customisation if wanted can be done through the config file. + Changed config file to XML format. + Builder command auto scans CGate if no config file exists. + Fixed interpretation of dimming commands. + PROD is the default state. In PROD, no option to stop comms. + Changed DEV to DEBUG for commonality. + Monitor and Talker attempt to always run unless in DEBUG state. + +V3.0.1 2013-11-22 + Fixed to work with C-Gate Version: v2.9.7 (build 2569), which returns + cbus addresses in the form NETWORK/APPLICATION/GROUP rather than + //PROJECT/NETWORK/APPLICATION/GROUP. + Add logging to aid debugging cbus_builder + Contributed by Jon Whitear + +V3.0.2 2013-11-25 + Add support for both formats of return code, i.e. NETWORK/APPLICATION/GROUP + and //PROJECT/NETWORK/APPLICATION/GROUP. + +V3.0.3 2013-11-28 + Test debug flag for logging statements. + +V4.0 2016-03-25 + Refactor cbus.pl into Clipsal_CBus.pm, CGate.pm, Group.pm, and Unit.pm, and + make CBus support more MisterHouse "native". + +=head1 LICENSE + +This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty +of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with this program; if not, write to the +Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +=cut + +1; diff --git a/lib/Clipsal_CBus/CGate.pm b/lib/Clipsal_CBus/CGate.pm new file mode 100644 index 000000000..d15c944de --- /dev/null +++ b/lib/Clipsal_CBus/CGate.pm @@ -0,0 +1,1102 @@ + +=head1 B + +=head2 SYNOPSIS + +CGate.pm - support for the Clipsal CGate interface. + +=head2 DESCRIPTION + +This module adds support for the Clispal CGtae interface. It is largely derived from the +original CBus support available in cbus.pl. See Clipsal_CBus.pm for detailed usage notes. + +=cut + +package Clipsal_CBus::CGate; + +use strict; +use Clipsal_CBus; + +#log levels +my $warn = 1; +my $notice = 2; +my $info = 3; +my $debug = 4; +my $trace = 5; + +@Clipsal_CBus::CGate::ISA = ( 'Generic_Item', 'Clipsal_CBus' ); + +=item C + + Instantiates a new object. + +=cut + +sub new { + my ($class) = @_; + my $self = new Generic_Item(); + + $$self{project} = $::config_parms{cbus_project_name}; + $$self{cbus_mht_filename} = + $::config_parms{code_dir} . "/" . $::config_parms{cbus_mht_file}; + + $$self{session_id} = undef; + $$self{cbus_units_config} = undef; + $$self{cbus_got_tree_list} = undef; + $$self{cbus_scanning_tree} = undef; + $$self{cbus_unit_list} = undef; + $$self{cbus_group_list} = undef; + $$self{cbus_scanning_cgate} = undef; + $$self{cbus_scan_last_addr_seen} = undef; + $$self{network_state} = undef; + $$self{addr_not_sync_ref} => {}; + $$self{cmd_list} => {}; + $$self{cbus_net_list} => {}; + $$self{cbus_app_list} => {}; + $$self{CBus_Sync} = new Generic_Item(); + $$self{sync_in_progress} = 0; + $$self{DELAY_CHECK_SYNC} = 10; + $$self{cbus_group_idx} = undef; + $$self{cbus_unit_idx} = undef; + $$self{request_cgate_scan} = 0; + + bless $self, $class; + + $self->monitor_start(); + $self->talker_start(); + + # Add hooks to the main loop to check the monitor and talker sockets for data on each pass. + &::MainLoop_pre_add_hook( sub { $self->monitor_check(); }, 'persistent' ); + &::MainLoop_pre_add_hook( sub { $self->talker_check(); }, 'persistent' ); + + # Add hook to generate voice commnds post reload + &::Reload_post_add_hook( \&Clipsal_CBus::generate_voice_commands, 1 ); + + return $self; +} + +=item C + + Scan CGate server to update the configuration. + +=cut + +sub scan_cgate { + my ($self) = @_; + + # Initiate scan of CGate data + # The scan is controlled by code in the Talker mh main loop code + $self->debug( "scan_cgate() Scanning CGate...", $notice ); + + # Cleanup from any previous scan and initialise flags/counters + @{ $$self{cbus_net_list} } = []; + + if ( defined $$self{project} ) { + $Clipsal_CBus::Talker->set( "project load " . $$self{project} ); + $Clipsal_CBus::Talker_last_sent = "project load " . $$self{project}; + + $Clipsal_CBus::Talker->set( "project use " . $$self{project} ); + $Clipsal_CBus::Talker_last_sent = "project use " . $$self{project}; + + $self->debug( "scan_cgate() Command - project start $$self{project}", + $notice ); + $Clipsal_CBus::Talker->set( "project start " . $$self{project} ); + $Clipsal_CBus::Talker_last_sent = "project start " . $$self{project}; + } + + $$self{request_cgate_scan} = 1; + $Clipsal_CBus::Talker->set("get cbus networks"); + $Clipsal_CBus::Talker_last_sent = "get cbus networks"; + +} + +sub add_address_to_hash { + my ( $self, $addr, $label ) = @_; + my $name = join "_", split " ", $label; + + #my ( $addr, $name ) = @_; + my $addr_type; + + if ( $addr =~ /\/p\/(\d+)/ ) { + + # Data is for a CBus device eg. switch, relay, dimmer + $addr_type = 'unit'; + $addr = $1; + } + else { + # Data is for a CBus "group" + $addr_type = 'group'; + } + + $self->debug( + "add_address_to_hash() Addr $addr is $name of type $addr_type", + $debug ); + + # Store the CBus name and address in the cbus_def hash + if ( $addr_type eq 'group' ) { + if ( not exists $Clipsal_CBus::Groups{$addr} ) { + $self->debug( + "add_address_to_hash() group not defined yet, adding $addr, $name", + $info + ); + + $Clipsal_CBus::Groups{$addr}{name} = $name; + $Clipsal_CBus::Groups{$addr}{label} = $label; + $Clipsal_CBus::Groups{$addr}{note} = + "Added by CBus scan $::Date_Now $::Time_Now"; + + $self->debug( "Address: $addr", $debug ); + $self->debug( "Name: $Clipsal_CBus::Groups{$addr}{name}", $debug ); + $self->debug( "Label: $Clipsal_CBus::Groups{$addr}{label}", + $debug ); + $self->debug( "Note: $Clipsal_CBus::Groups{$addr}{note}", $debug ); + + } + else { + $self->debug( "add_address_to_hash() group $addr already exists", + $info ); + } + } + elsif ( $addr_type eq 'unit' ) { + if ( not exists $Clipsal_CBus::Units{$addr} ) { + $self->debug( + "add_address_to_hash() unit not defined yet, adding $addr, $name", + $info + ); + $Clipsal_CBus::Units{$addr} = { + name => $name, + note => "Added by MisterHouse $::Date_Now $::Time_Now" + }; + } + else { + $self->debug( "add_address_to_hash() unit $addr already exists", + $info ); + } + } + +} + +# +# Setup to sync levels of all known addresses +# +sub start_level_sync { + my ($self) = @_; + + #return if not defined $Clipsal_CBus::Groups; + + $self->debug( "Syncing MisterHouse to CBus", $notice ); + + $$self{CBus_Sync}->set('off'); + $$self{sync_in_progress} = 1; + %{ $$self{addr_not_sync} } = %Clipsal_CBus::Groups; + + $self->attempt_level_sync(); +} + +# +# Send commands to synchronise the Misterhouse level to CBus +# +sub attempt_level_sync { + my ($self) = @_; + + my @count = keys %{ $$self{addr_not_sync} }; + $self->debug( "attempt_level_sync() count=" . @count, $info ); + + if ( not %{ $$self{addr_not_sync} } ) { + $self->debug( "Sync to CGate complete", $notice ); + $$self{CBus_Sync}->set('on'); + $$self{sync_in_progress} = 0; + + } + else { + $self->debug( "attempt_level_sync() list:@count", $info ); + + my @addresses = keys %{ $$self{addr_not_sync} }; + foreach my $addr (@addresses) { + + # Skip if CBus scene group address + if ( $addr =~ /\/\/.+\/\d+\/202\/\d+/ + or $addr =~ /\/\/.+\/\d+\/203\/\d+/ ) + { + delete $$self{addr_not_sync}->{$addr}; + next; + } + $Clipsal_CBus::Talker->set("[MisterHouse $addr] get $addr level"); + $Clipsal_CBus::Talker_last_sent = + "[MisterHouse $addr] get $addr level"; + + } + + &::eval_with_timer( '$CGATE->attempt_level_sync()', + $$self{DELAY_CHECK_SYNC} ); + } +} + +sub cbus_update { + my ( $self, $addr, $cbus_state, $set_by ) = @_; + + my $object_name = $Clipsal_CBus::Groups{$addr}{object_name}; + my $set_command = $object_name . "->set('$cbus_state','$set_by');"; + + $self->debug( + "cbus_update triggering set() for object $object_name with state $cbus_state", + $debug + ); + $self->debug( "cbus_update set() command: $set_command", $trace ); + + package main; + eval $set_command; + print "Error in cbus set command: $@\n" if $@; + + package Clipsal_CBus::CGate; +} + +sub write_mht_file { + my ($self) = @_; + + my $count = 0; + $self->debug( "Writing MHT file $$self{cbus_mht_filename}", $notice ); + + open( CF, ">$$self{cbus_mht_filename}" ) + or $self->debug( + "write_mht_file() Could not open $$self{cbus_mht_filename}: $!", + $warn ); + + print CF "# CBus mht file auto-generated on $::Date_Now at $::Time_Now.\n"; + print CF "\n"; + print CF + "# This file may be overwritten - do not edit. Instead, copy this file to \n"; + print CF "# (for example) cbus.mht and edit as required.\n"; + print CF "\n"; + print CF "Format = A\n"; + print CF "\n"; + print CF "#TYPE, Address, Name, GroupList, Other\n"; + print CF "\n"; + print CF "CBUS_CGATE, CGATE\n"; + print CF "\n"; + + print CF "# CBus trigger group addresses.\n"; + print CF "\n"; + + foreach my $address ( sort keys %Clipsal_CBus::Groups ) { + if ( $address =~ /\/\d+\/(\d+)\/\d+/ ) { + my $application = $1; + next if not $application == 202; + } + + my $name = $Clipsal_CBus::Groups{$address}{name}; + my $label = $Clipsal_CBus::Groups{$address}{label}; + + print CF "CBUS_TRIGGER, $address, $name, All_CBus, $label\n"; + } + + print CF "\n"; + print CF "# CBus lighting group addresses.\n"; + print CF "\n"; + + foreach my $address ( sort keys %Clipsal_CBus::Groups ) { + if ( $address =~ /\/\d+\/(\d+)\/\d+/ ) { + my $application = $1; + next if not $application == 56; + } + my $name = $Clipsal_CBus::Groups{$address}{name}; + my $label = $Clipsal_CBus::Groups{$address}{label}; + + print CF "CBUS_GROUP, $address, $name, All_CBus, $label\n"; + + } + + print CF "\n"; + print CF "# CBus Unit addresses.\n"; + print CF "\n"; + + foreach my $address ( sort keys %Clipsal_CBus::Units ) { + my $name = $Clipsal_CBus::Units{$address}{name}; + my $label = $Clipsal_CBus::Units{$address}{label}; + + print CF "CBUS_UNIT, $address, $name, $label\n"; + + } + + print CF "#\n#\n# EOF\n#\n#\n"; + + close(CF) + or $self->debug( + "write_mht_file() Could not close $$self{cbus_mht_filename}: $!", + $warn ); + + $self->debug( + "write_mht_file() Completed CBus build to $$self{cbus_mht_filename}", + $notice ); + +} + +############################################################################## +############################################################################## +############################################################################## +############################################################################## +########### ############## +########### CBus MONITOR ############## +########### ############## +############################################################################## +############################################################################## +############################################################################## +############################################################################## + +# Monitor functions +# +sub monitor_start { + my ($self) = @_; + + # Start the CBus listener (monitor) + + if ( $Clipsal_CBus::Monitor->active() ) { + $self->debug( "Monitor already running, skipping start", $notice ); + } + else { + $$self{monitor_retry} = 0; + if ( $Clipsal_CBus::Monitor->start() ) { + $self->debug( "Monitor started", $notice ); + } + else { + $self->debug( "Monitor failed to start", $warn ); + } + } +} + +sub monitor_stop { + my ($self) = @_; + + # Stop the CBus listener (monitor) + + if ( not $Clipsal_CBus::Monitor->active() ) { + $self->debug( "Monitor isn't active, skipping stop", $notice ); + } + else { + #$$self{monitor_retry} = 0; + if ( $Clipsal_CBus::Monitor->stop() ) { + $self->debug( "Monitor stopped", $notice ); + } + else { + $self->debug( "Monitor failed to stop", $warn ); + } + } +} + +sub monitor_status { + my ($self) = @_; + + # Return the status of the CBus listener (monitor) + + if ( $Clipsal_CBus::Monitor->active() ) { + $self->debug( "Monitor is active.", $notice ); + } + else { + $self->debug( "Monitor is NOT running", $notice ); + } +} + +sub monitor_check { + my ($self) = @_; + + # Monitor Voice Command / Menu processing + if ( my $data = $::CBus_Monitor_v->said() ) { + if ( $data eq 'Status' ) { + $self->monitor_status(); + } + else { + $self->debug( "Monitor: command $data is not implemented", $warn ); + } + } + + #Process monitor socket input + if ( my $monitor_msg = $Clipsal_CBus::Monitor->said() ) { + $self->debug( "Monitor message: $monitor_msg", $debug ); + + my @cg = split / /, $monitor_msg; + my $cg_code = $cg[1]; + + unless ( $cg_code == 730 ) { # only code 730 are of interest + $self->debug( + "Monitor ignoring uninteresting message type $cg_code", + $debug ); + return; + } + + my $cg_time = $cg[0]; + my $cg_addr = $cg[2]; + my $cg_action = $cg[4]; + my $cg_level = $cg[5]; + my $cg_source = $cg[6]; + my $cg_ramptime = $cg[7]; + my $cg_sessionId = $cg[8]; + my $cg_commandId = $cg[9]; + + my $level = abs( substr( $cg_level, 6, 3 ) ); + my $source = substr( $cg_source, 11 ); + my $cbus_state = 0; + + $self->debug( "Monitor processing message type $cg_code", $debug ); + + # Determine SOURCE of the command + my $could_be_ramp_starting = 1; + + if ( $cg_sessionId =~ /$$self{session_id}/ ) { + + # Monitor message includes the current Misterhouse CBus session ID, so the message is + # a response to a talker message sent by misterhouse. + $source = "MisterHouse via Session ID"; + } + elsif ( $cg_commandId =~ /commandId=(.+)/ ) { + + # If commandId is present then CGate sent the command + # Monitor message includes a command ID, so CGate sent the command. + my $command_id = $1; + + # CGate doesn't send a "ramp starting" message + $could_be_ramp_starting = 0; + + if ( $command_id =~ /^\d+/ ) { + + # Assume that Toolkit is the only software that uses only a count + # for it's command IDs. Would have been helpful if Clipsal + # had put a label specifying Toolkit as well.... + $source = "ToolKit"; + } + elsif ( $command_id =~ /MisterHouse/ ) { + $source = "MisterHouse via Command ID"; + } + else { + # If other software issues CGATE commands using the [] label, + # ie. [DudHomeControl] on //HOME/254/56/1 + # then the source in MH will be shown as "DudHomeControl". + $source = $command_id; + + # Otherwise, MH will just show that CGate was used. + $source = "CGate" if $source eq '{none}'; + } + } + else { + # the source was a CBus unit. + $source = "cbus unit: $Clipsal_CBus::Units{$source}{name}"; + } + + $self->debug( "Monitor source is $source", $debug ); + + # Determine what level is being reported + my $ramping; + my $state_speak; + $cg_ramptime =~ s/ramptime=//i; + + ### if ($could_be_ramp_starting and $cg_ramptime > 0) { + if ( $cg_ramptime > 0 ) { + $self->debug( "Monitor ramptime $cg_ramptime detected", $debug ); + + # The group has started ramping + if ( $level == 255 ) { + $cbus_state = 'on'; + $ramping = 'UP/ON'; + $state_speak = 'ramping UP'; + } + elsif ( $level == 0 ) { + $cbus_state = 'off'; + $ramping = 'DOWN/OFF'; + $state_speak = 'ramping DOWN'; + } + else { + my $plevel = $level / 255 * 100; + $ramping = 'UP/DOWN'; + $cbus_state = sprintf( "%.0f%%", $plevel ); + } + } + else { + if ( $level == 255 ) { + $cbus_state = 'on'; + $state_speak = 'set to ON'; + + } + elsif ( $level == 0 ) { + $cbus_state = 'off'; + $state_speak = 'set to OFF'; + + } + else { + my $plevel = $level / 255 * 100; + $cbus_state = sprintf( "%.0f%%", $plevel ); + $state_speak = sprintf( "dim to %.0f%%", $plevel ); + } + $self->debug( "Monitor not ramping - level set to $cbus_state", + $debug ); + } + + my $cbus_label = $Clipsal_CBus::Groups{$cg_addr}{label}; + + if ( $source eq 'MisterHouse via Session ID' ) { + + # This is a Reflected mesg, we will ignore + $self->debug( "Monitor ignoring reflected message from $source", + $debug ); + } + elsif ( $source eq 'MisterHouse via Command ID' ) { + + # This is a Reflected mesg, we will ignore + $self->debug( "Monitor ignoring reflected message from $source", + $debug ); + } + elsif ( not defined $cbus_label ) { + $self->debug( + "Monitor UNKNOWN Address $cg_addr $state_speak by $source", + $debug ); + } + else { + # The source is a CBus unit, CGate, Toolkit, or some other software. Trigger an object set(). + $self->debug( "Monitor $cbus_label ramping $ramping by $source", + $debug ) + if ($ramping); + $self->cbus_update( $cg_addr, $cbus_state, $source ); + } + } + +} + +############################################################################## +############################################################################## +############################################################################## +############################################################################## +########### ############## +########### CBus TALKER ############## +########### ############## +############################################################################## +############################################################################## +############################################################################## +############################################################################## + +# +# Talker functions +# +sub talker_start { + my ($self) = @_; + + # Starts the CBus command driver (Talker) + + if ( $Clipsal_CBus::Talker->active() ) { + $self->debug( "Talker already running, skipping start", $notice ); + } + else { + #set $Clipsal_CBus_CGate::CBus_Sync = "OFF"; + $$self{talker_retry} = 0; + if ( $Clipsal_CBus::Talker->start() ) { + $self->debug( "Talker started", $notice ); + } + else { + $self->debug( "Talker failed to start", $notice ); + } + } +} + +sub talker_stop { + my ($self) = @_; + + # Stops the CBus command driver (Talker) + + if ( not $Clipsal_CBus::Talker->active() ) { + $self->debug( "Talker isn't active, skipping stop", $notice ); + } + else { + #set $Clipsal_CBus_CGate::CBus_Sync = "OFF"; + if ( $Clipsal_CBus::Talker->stop() ) { + $self->debug( "Talker stopped", $notice ); + } + else { + $self->debug( "Talker failed to stop", $warn ); + } + } +} + +sub talker_status { + my ($self) = @_; + + # Returns the status of the CBus command driver (Talker) + + if ( $Clipsal_CBus::Talker->active() ) { + $self->debug( "Talker is active.", $notice ); + } + else { + $self->debug( "Talker is not running", $notice ); + } +} + +sub talker_check { + my ($self) = @_; + + # Talker Voice Command / Menu processing + if ( my $data = $::CBus_Talker_v->said() ) { + if ( $data eq 'Status' ) { + $self->talker_status(); + + } + elsif ( $data eq 'Scan' ) { + $self->scan_cgate(); + + } + else { + $self->debug( "Talker: command $data is not implemented", $warn ); + } + } + + # Process data returned from CBus server after a command is sent + # + if ( my $talker_msg = $Clipsal_CBus::Talker->said() ) { + my $msg_code = -1; + my $msg_id; + + if ( $talker_msg =~ /(\[.+\]\s+)?(\d\d\d)/ ) { + $msg_id = $1; + $msg_code = $2; + } + + $self->debug( "Talker received: $talker_msg", $debug ); + + ###### Message code 200: Completed successfully + + if ( $msg_code == 200 ) { + $self->debug( "Talker Cmd OK - $talker_msg", $debug ); + } + + ###### Message code 201: Service ready + + elsif ( $msg_code == 201 ) { + $self->debug( "Talker Comms established - $talker_msg", $notice ); + + # Newly started comms, therefore find the networks available + # then we will wait until CGate has sync'ed with the network + $$self{request_cgate_scan} = 0; + $Clipsal_CBus::Talker->set("session_id"); + $Clipsal_CBus::Talker_last_sent = "session_id"; + + if ( not defined $$self{project} ) { + $self->debug( + "Talker ***ERROR*** Set \$cbus_project_name in mh.ini", + $warn ); + } + elsif ( keys %Clipsal_CBus::Groups == 0 ) { + + #we have no pre-defined CBus group objects loaded into the hash, so kick off a scan + $self->debug( + "Talker - no existing CBus Group objects. Initiating Scan.", + $warn + ); + $self->scan_cgate(); + } + else { + # initial a sync + $Clipsal_CBus::Talker->set("project load $$self{project}"); + $Clipsal_CBus::Talker_last_sent = + "project load $$self{project}"; + + $Clipsal_CBus::Talker->set("project use $$self{project}"); + $Clipsal_CBus::Talker_last_sent = "project use $$self{project}"; + + $Clipsal_CBus::Talker->set("project start $$self{project}"); + $Clipsal_CBus::Talker_last_sent = + "project start $$self{project}"; + + $Clipsal_CBus::Talker->set("get cbus networks"); + $Clipsal_CBus::Talker_last_sent = "get cbus networks"; + } + } + + ###### Message code 300: Object information, for example: 300 1/56/1: level=200 + + elsif ( $msg_code == 300 ) { + + if ( $talker_msg =~ /(sessionID=.+)/ ) { + $$self{session_id} = $1; # Set global session ID + $self->debug( "Talker Session ID is \"$$self{session_id}\"", + $notice ); + + } + elsif ( $talker_msg =~ /networks=(.+)/ ) { + my $netlist = $1; + $self->debug( "Talker network list: $netlist", $notice ); + @{ $self->{cbus_net_list} } = split /,/, $netlist; + + # Request state of network + $self->debug( + "Talker sent: get " . $self->{cbus_net_list}[0] . " state", + $debug + ); + $Clipsal_CBus::Talker->set( + "get " . $self->{cbus_net_list}[0] . " state" ); + $Clipsal_CBus::Talker_last_sent = + "get " . $self->{cbus_net_list}[0] . " state"; + + } + elsif ( $talker_msg =~ /state=(.+)/ ) { + my $network_state = $1; + $self->debug( "Talker CGate Status - $talker_msg", $debug ); + if ( $network_state ne "ok" ) { + $Clipsal_CBus::Talker->set( + "get " . $self->{cbus_net_list}[0] . " state" ); + $Clipsal_CBus::Talker_last_sent = + "get " . $self->{cbus_net_list}[0] . " state"; + } + else { + if ( $$self{request_cgate_scan} ) { + + # This state request was part of scanning startup + $self->debug( + "Talker state request was part of scanning startup", + $debug + ); + + $$self{cbus_scanning_cgate} = 1; # Set scanning flag + $$self{request_cgate_scan} = 0; + } + else { + # If not a scan, then is a startup sync being kicked off + $self->debug( "Not a scan... starting level sync", + $info ); + $self->start_level_sync(); + } + } + + } + elsif ( + $talker_msg =~ /(\/\/[\w|\d]+\/\d+\/\d+\/\d+):\s+level=(.+)/ ) + { + my ( $addr, $level ) = ( $1, $2 ); + + my $cbus_state; + if ( $level == 255 ) { + $cbus_state = 'on'; + } + elsif ( $level == 0 ) { + $cbus_state = 'off'; + } + else { + my $plevel = $level / 255 * 100; + $cbus_state = sprintf( "%.0f%%", $plevel ); + } + + # Store new level from sync response + $self->cbus_update( $addr, $cbus_state, "MisterHouseSync" ); + + delete $self->{addr_not_sync} > + {$addr}; # Remove from not sync'ed list + my $name = $Clipsal_CBus::Groups{$addr}{name}; + $self->debug( "Talker $name ($addr) is $cbus_state", $info ) + if $cbus_state ne 'off'; + } + else { + $self->debug( "Talker UNEXPECTED 300 msg \"$talker_msg\"", + $info ); + } + + } + + ###### Message code 320: Tree information. Returned from the tree command, which returns a list of units + ###### followed by a list of groups, ordered by application. + + elsif ( $msg_code == 320 ) { + if ( not $$self{cbus_got_tree_list} ) { + if ( not $$self{cbus_units_config} ) { + if ( $talker_msg =~ /Applications/ ) { + + #we've started listing applications and groups + $$self{cbus_units_config} = 1; + } + elsif ( + $talker_msg =~ /(\/\/.+\/\d+\/p\/\d+).+type=(.+) app/ ) + { + + # CGate is listing CBus "units" (input and output) + $self->debug( "Talker scanned addr=$1 is type $2", + $debug ); + + # Store unit on a list for later scanning of details + push( @{ $$self{cbus_unit_list} }, $1 ); + + } + + } + else { + # CGate is listing CBus "groups" + if ( $talker_msg =~ /end/ ) { + + #we've finished scanning the tree + $self->debug( + "Talker end of CBus scan data, got tree list", + $notice ); + $$self{cbus_got_tree_list} = 1; + } + elsif ( + #this is an applcation response, e.g. 320 Application 56 ($38) [lighting] + $talker_msg =~ /Application (\d+).+\[(.+)\]/ + ) + { + $self->debug( "Talker found application $1 of type $2", + $notice ); + + # Store application on a list + $$self{cbus_app_list}{$1}{type} = $2; + } + elsif ( + #this is a group response, e.g. 320 //HOME/254/56/0 ($0) level=0 state=ok units=2,12 + $talker_msg =~ /(\/\/.+\/\d+\/\d+\/\d+).+level=(\d+)/ + ) + { + $self->debug( "Talker scanned group=$1 at level $2", + $info ); + + # Store group on a list for later scanning of details + push( @{ $$self{cbus_group_list} }, $1 ); + } + } + } + } + + ###### Message code 342: DBGet response (not documented in CGate Server Guide 1.0.) + + elsif ( $msg_code == 342 ) { + if ( $$self{cbus_scanning_cgate} ) { + + $self->debug( "Talker message 342 response data: $talker_msg", + $debug ); + + if ( $talker_msg =~ /\d+\s+(\d+\/[a-z\d]+\/\d+)\/TagName=(.+)/ ) + { + + #response matched against "new" format, i.e. network/app/group + my ( $addr, $name ) = ( $1, $2 ); + $addr = "//$$self{project}/$addr"; + + $$self{cbus_scan_last_addr_seen} = $addr; + + # $name =~ s/ /_/g; Change spaces, depends on user usage... + $self->add_address_to_hash( $addr, $name ); + + } + elsif ( $talker_msg =~ + /(\/\/.+\/\d+\/[a-z\d]+\/\d+)\/TagName=(.+)/ ) + { + #response matched against "old" format, i.e. //project/network/app/group + my ( $addr, $name ) = ( $1, $2 ); + + $$self{cbus_scan_last_addr_seen} = $addr; + + # $name =~ s/ /_/g; Change spaces, depends on user usage... + $self->add_address_to_hash( $addr, $name ); + + } + $self->debug( "Talker end message", $info ); + } + } + + ###### Message code 401: Bad object or device ID + + elsif ( $msg_code == 401 ) { + $self->debug( "Talker $talker_msg", $info ); + } + + ###### Message code 408: Indicates that a SET, GET or other method + ###### failed for a given object + + elsif ( $msg_code == 408 ) { + $self->debug( "Talker **** Failed Cmd - $talker_msg", $warn ); + $self->debug( + "Talker last sent command = $Clipsal_CBus::Talker_last_sent", + $warn ); + + if ( $msg_id =~ /\[MisterHouse-(\d+)\]/ ) { + my $cmd_num = $1; + my $cmd = $self->{cmd_list}{$cmd_num}; + if ( $cmd ne "" ) { + $self->debug( "Talker Trying command again - $cmd", + $warn ); + $Clipsal_CBus::Talker->set($cmd); + $Clipsal_CBus::Talker_last_sent = $cmd; + $self->{cmd_list}{$cmd_num} = ""; + } + else { + $self->debug( "Talker 2nd failure - abandoning command", + $warn ); + } + } + } + + ###### Message code unhandled + + else { + $self->debug( "Talker Cmd port - UNHANDLED: $talker_msg", $warn ); + } + } + + # + # Control scanning of the CGate configuration + # + if ( $$self{cbus_scanning_cgate} ) { + if ( not $$self{cbus_scanning_tree} ) { + if ( my $network = pop @{ $$self{cbus_net_list} } ) { + + # Cleanup from any previous scan and initialise flags/counters + $$self{cbus_units_config} = 0; + $$self{cbus_got_tree_list} = 0; + undef @{ $$self{cbus_group_list} }; + undef @{ $$self{cbus_unit_list} }; + undef $$self{cbus_scan_last_addr_seen}; + $$self{cbus_group_idx} = 0; + $$self{cbus_unit_idx} = 0; + + # Request from CGate a list of addresses on network + $network = "//$$self{project}/$network"; + $self->debug( "Talker scanning network $network", $notice ); + $self->debug( "Talker sent: tree $network", $debug ); + $Clipsal_CBus::Talker->set("tree $network"); + $Clipsal_CBus::Talker_last_sent = "tree $network"; + + $$self{cbus_scanning_tree} = 1; + + } + else { + # All networks scanned - set completion flag + ### FIXME - RichardM test with two networks?? + $self->debug( "Talker leaving scanning mode", $notice ); + $$self{cbus_scanning_cgate} = 0; + $self->debug( "CBus server scan complete", $notice ); + $self->write_mht_file(); + } + + } + elsif ( $$self{cbus_got_tree_list} ) { + if ( $$self{cbus_group_idx} < @{ $$self{cbus_group_list} } ) { + my $group = $$self{cbus_group_list}[ $$self{cbus_group_idx}++ ]; + $self->debug( "Talker dbget group $group", $info ); + $Clipsal_CBus::Talker->set("dbget $group/TagName"); + $Clipsal_CBus::Talker_last_sent = "dbget $group/TagName"; + + } + elsif ( $$self{cbus_unit_idx} < @{ $$self{cbus_unit_list} } ) { + my $unit = $$self{cbus_unit_list}[ $$self{cbus_unit_idx}++ ]; + $self->debug( "Talker dbget unit $unit", $info ); + $Clipsal_CBus::Talker->set("dbget $unit/TagName"); + $Clipsal_CBus::Talker_last_sent = "dbget $unit/TagName"; + + } + else { + if ( $$self{cbus_scan_last_addr_seen} eq + $$self{cbus_unit_list}[ $#{ $$self{cbus_unit_list} } ] ) + { + # Tree Scan complete - set tree completion flag + $self->debug( "Talker leaving scanning mode", $notice ); + $$self{cbus_scanning_tree} = 0; + } + } + + } + else { + # We are in scanning_tree mode, and waiting for response to the + # TREE command. The TREE command lists each address on the particular + # network. Then we will "dbget" each address. (That will start when + # cbus_got_tree_list becomes true. + } + } + +} + +=head1 AUTHOR + + Richard Morgan, omegaATbigpondDOTnetDOTau + Andrew McCallum, Mandoon Technologies, andyATmandoonDOTcomDOTau + Jon Whitear, jonATwhitearDOTorg + +=head1 VERSION HOSTORY + + 03-12-2001 + Modified to support c-gate 1.5 + 23-06-2002 + Monitor: Source name now works, and shows 'MH' is source 0 + 05-07-2002 + Modified for cbus_dat.csv input file support + Added groups and set_info support + 06-07-2002 + Minor changes to support new cbus_builder + Modified to support global %cbus_data hash + removed make_cbus_file(), replaced with cbus_builder.pl + 11-07-2002 + Added announce flag to cbus_dat.csv, and conditional speak flag $announce + 19-09-2002 + Fixed bug in cbus_set() that prevented dimming numeric % set values + being accepted. Dimming now works. + 21-09-2002 + Modified cbus_groups and cbus_catagories to read from input file + rather than hard coded + Put in config item cbus_category_prefix + Comments in input file now allowed + Fixed some other minor things + 22-09-2002 V2.0 + Collapsed cbus_talker.pl, cbus_builder.pl and cbus_monitor.pl + into one new file, cbus.pl. Now issued as V2.0. + + V2.1 Fixed up some menu uglies. + Improved coding in monitor loop + Fixed up code labels, docs etc + + V2.2 Changed all speak() calls to say 'C-Bus' rather than 'CBus', so the diction is correct + + V2.2.1 Fixed minor bug in cbus monitor start voice command + + V2.2.2 Implemented; + oneshot device type + cbus_oneshot_log config param + + V2.2.3 Made the dump_cbus_data format pretty HTML tables + + V3.0 2008-02-04 + Fixed to work with C-Gate Version: v2.6.1 (build 2236) + Latest version as of June 2008 + Now reports the name of the source unit that modified a group level. + Added ability to scan CGate for groups and output to config file. + *** Configuration only requires running Builder to scan cgate and + *** build XML file, then commanding MH to "reload code". Job Done. + *** Customisation if wanted can be done through the config file. + Changed config file to XML format. + Builder command auto scans CGate if no config file exists. + Fixed interpretation of dimming commands. + PROD is the default state. In PROD, no option to stop comms. + Changed DEV to DEBUG for commonality. + Monitor and Talker attempt to always run unless in DEBUG state. + + V3.0.1 2013-11-22 + Fixed to work with C-Gate Version: v2.9.7 (build 2569), which returns + cbus addresses in the form NETWORK/APPLICATION/GROUP rather than + //PROJECT/NETWORK/APPLICATION/GROUP. + Add logging to aid debugging cbus_builder + Contributed by Jon Whitear + + V3.0.2 2013-11-25 + Add support for both formats of return code, i.e. NETWORK/APPLICATION/GROUP + and //PROJECT/NETWORK/APPLICATION/GROUP. + + V3.0.3 2013-11-28 + Test debug flag for logging statements. + + V4.0 2016-03-25 + Refactor cbus.pl into Clipsal_CBus.pm, CGate.pm, Group.pm, and Unit.pm, and + make CBus support more MisterHouse "native". + +=head1 LICENSE + + This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty + of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program; if not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +=cut + +1; + diff --git a/lib/Clipsal_CBus/Group.pm b/lib/Clipsal_CBus/Group.pm new file mode 100644 index 000000000..ca4c5873f --- /dev/null +++ b/lib/Clipsal_CBus/Group.pm @@ -0,0 +1,193 @@ + +=head1 B + +=head2 SYNOPSIS + +Group.pm - support for Clipsal CBus output groups. + +=head2 DESCRIPTION + +This module is a child of Clispal_CBus. It provides a MisterHouse object +that corresponds to a CBus output group, with a set() method that overides +that inherited from it's GenericItem parent. + +Note that there is no provision for an output group to have a "relay" type +behaviour (i.e. states "on" and "off" only) as output groups can be mapped +to multiple CBus output unit channels, which may include dimmer and/or relay +channels. + +=cut + +package Clipsal_CBus::Group; + +use strict; +use Clipsal_CBus; + +#log levels +my $warn = 1; +my $notice = 2; +my $info = 3; +my $debug = 4; +my $trace = 5; + +@Clipsal_CBus::Group::ISA = ( 'Generic_Item', 'Clipsal_CBus' ); + +=item C + + Instantiates a new CBus group object. + +=cut + +sub new { + my ( $class, $address, $name, $label ) = @_; + my $self = new Generic_Item(); + + bless $self, $class; + + my $object_name = "\$" . $name; + my $object_name_v = $object_name . '_v'; + + &::print_log("[Clipsal CBus] New group object $object_name at $address"); + + $self->set_states( split ',', + 'on,off,5%,10%,20%,30%,40%,50%,60%,70%,80%,90%' ); + $self->set_label($label); + $$self{ramp_speed} = $::config_parms{cbus_ramp_speed}; + $$self{address} = $address; + + #Add this object to the CBus object hash. + $Clipsal_CBus::Groups{$address}{object_name} = $object_name; + $Clipsal_CBus::Groups{$address}{name} = $name; + $Clipsal_CBus::Groups{$address}{label} = $label; + $Clipsal_CBus::Groups{$address}{note} = "Added at object creation"; + + return $self; +} + +=item C + + Places the value into the state field (e.g. set $light on) at the start of the next mh pass, and reflects that state + to the CBus group via the CBus talker. + + Depending on the source of the set() call, one or both of these actions may not be required. Scenarios are as follows: + + 1) A CBus group is set to a new value by a CBus unit (e.g. a switch). This is seen by the CBus monitor, and the source + of set_by is passed as "cbus". The CBus monitor calls set(), which reflects the new $state in the MH object. + + 2) A MH object is set by the web interface or user code (or other MH function), by calling the objects set() method. + The set() method sets the MH object $state, and sends the corresponding CBus command to the CBus Talker socket. + + (optional) set_by overrides the default set_by value. + + (optional) respond overrides the default respond value. + +=cut + +sub set { + my ( $self, $state, $set_by, $respond ) = &Generic_Item::_set_process(@_); + &Generic_Item::set_states_for_next_pass( $self, $state, $set_by, $respond ) + if $self; + + my $orig_state = $state; + my $cbus_label = $self->{label}; + my $address = $$self{address}; + my $speed = $$self{ramp_speed}; + + $self->debug( "$cbus_label set to state $state by $set_by", $info ); + + if ( $set_by =~ /cbus/ ) { + + # This was a Recursive set, we are ignoring + $self->debug( "set() by CBus - no CBus update required", $debug ); + return; + } + + if ( $set_by =~ /MisterHouseSync/ ) { + + # This was a Recursive set, we are ignoring + $self->debug( "set() by MisterHouse sync - no CBus update required", + $debug ); + return; + } + + # Get rid of any % signs in the $Level value + $state =~ s/%//g; + + if ( ( $state =~ /on/ ) || ( $state =~ /ON/ ) ) { + $state = 255; + + } + elsif ( ( $state eq /off/ ) || ( $state eq /OFF/ ) ) { + $state = 0; + + } + elsif ( ( $state <= 100 ) && ( $state >= 0 ) ) { + $state = int( $state / 100.0 * 255.0 ); + + } + else { + $self->debug( "invalid level \'$state\' passed to set()", $warn ); + return; + } + + my $cmd_log_string = "RAMP $cbus_label set $state, speed=$speed"; + $self->debug( "$cmd_log_string", $debug ); + + my $ramp_command = + "[MisterHouse-$Clipsal_CBus::Command_Counter] RAMP $address $state $speed\n"; + $Clipsal_CBus::Talker->set($ramp_command); + $Clipsal_CBus::Talker_last_sent = $ramp_command; + $Clipsal_CBus::Command_Counter = 0 + if ( + ++$Clipsal_CBus::Command_Counter > $Clipsal_CBus::Command_Counter_Max ); +} + +=item C + + Returns a hash of voice commands where the key is the voice command name and the + value is the perl code to run when the voice command name is called. + + Higher classes which inherit this object may add to this list of voice commands by + redefining this routine while inheriting this routine using the SUPER function. + + This routine is called by L to generate the + necessary voice commands. + +=cut + +sub get_voice_cmds { + my ($self) = @_; + my %voice_cmds = ( + + 'set on' => $self->get_object_name . '->set( 100 , "Voice Command")', + 'set off' => $self->get_object_name . '->set( 0 , "Voice Command")' + ); + + return \%voice_cmds; +} + +=head1 AUTHOR + + This code is based on the original cbus.pl implementation by: + + Richard Morgan, omegaATbigpondDOTnetDOTau + Andrew McCallum, Mandoon Technologies, andyATmandoonDOTcomDOTau + + It was refactored to make it more MisterHouse "native" by: + + Jon Whitear, jonATwhitearDOTorg + +=head1 LICENSE + + This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty + of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program; if not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +=cut + +1; diff --git a/lib/Clipsal_CBus/TriggerGroup.pm b/lib/Clipsal_CBus/TriggerGroup.pm new file mode 100644 index 000000000..5c5760a3f --- /dev/null +++ b/lib/Clipsal_CBus/TriggerGroup.pm @@ -0,0 +1,183 @@ + +=head1 B + +=head2 SYNOPSIS + +TriggerGroup.pm - support for Clipsal CBus trigger groups. + +=head2 DESCRIPTION + +This module is a child of Clispal_CBus. It provides a MisterHouse object +that corresponds to a CBus trigger group, with a set() method that overides +that inherited from it's GenericItem parent. + +=cut + +package Clipsal_CBus::TriggerGroup; + +use strict; +use Clipsal_CBus; + +#log levels +my $warn = 1; +my $notice = 2; +my $info = 3; +my $debug = 4; +my $trace = 5; + +@Clipsal_CBus::TriggerGroup::ISA = ( 'Generic_Item', 'Clipsal_CBus' ); + +=item C + + Instantiates a new CBus trigger group object. + +=cut + +sub new { + my ( $class, $address, $name, $label ) = @_; + my $self = new Generic_Item(); + + bless $self, $class; + + my $object_name = "\$" . $name; + my $object_name_v = $object_name . '_v'; + + &::print_log( + "[Clipsal CBus] New trigger group object $object_name at $address"); + + #$self->set_states( split ',','on,off,5%,10%,20%,30%,40%,50%,60%,70%,80%,90%' ); + $self->set_label($label); + $$self{address} = $address; + + #Add this object to the CBus object hash. + $Clipsal_CBus::Groups{$address}{object_name} = $object_name; + $Clipsal_CBus::Groups{$address}{name} = $name; + $Clipsal_CBus::Groups{$address}{label} = $label; + $Clipsal_CBus::Groups{$address}{note} = "Added at object creation"; + + return $self; +} + +=item C + + Places the value into the state field (e.g. set $light on) at the start of the next mh pass, and reflects that state + to the CBus group via the CBus talker. + + Depending on the source of the set() call, one or both of these actions may not be required. Scenarios are as follows: + + 1) A CBus group is set to a new value by a CBus unit (e.g. a switch). This is seen by the CBus monitor, and the source + of set_by is passed as "cbus". The CBus monitor calls set(), which reflects the new $state in the MH object. + + 2) A MH object is set by the web interface or user code (or other MH function), by calling the objects set() method. + The set() method sets the MH object $state, and sends the corresponding CBus command to the CBus Talker socket. + + (optional) set_by overrides the default set_by value. + + (optional) respond overrides the default respond value. + + The CBus syntax is: "TRIGGER" SP "EVENT" SP trigger-group-address SP action-selector + +=cut + +sub set { + my ( $self, $state, $set_by, $respond ) = &Generic_Item::_set_process(@_); + &Generic_Item::set_states_for_next_pass( $self, $state, $set_by, $respond ) + if $self; + + my $orig_state = $state; + my $cbus_label = $self->{label}; + my $address = $$self{address}; + + $self->debug( "$cbus_label set to action selector $state by $set_by", + $info ); + + if ( $set_by =~ /cbus/ ) { + + # This was a Recursive set, we are ignoring + $self->debug( "set() by CBus - no CBus update required", $debug ); + return; + } + + if ( $set_by =~ /MisterHouseSync/ ) { + + # This was a Recursive set, we are ignoring + $self->debug( "set() by MisterHouse sync - no CBus update required", + $debug ); + return; + } + + # Get rid of any % signs in the $Level value + $state =~ s/%//g; + + if ( ( $state =~ /on/ ) || ( $state =~ /ON/ ) ) { + $state = 255; + + } + elsif ( ( $state eq /off/ ) || ( $state eq /OFF/ ) ) { + $state = 0; + + } + elsif ( ( $state <= 255 ) && ( $state >= 0 ) ) { + + #$state = int( $state / 100.0 * 255.0 ); + + } + else { + $self->debug( "invalid level \'$state\' passed to set()", $warn ); + return; + } + + my $cmd_log_string = "TRIGGER EVENT $cbus_label set action selector $state"; + $self->debug( "$cmd_log_string", $debug ); + + my $command = + "[MisterHouse-$Clipsal_CBus::Command_Counter] TRIGGER EVENT $address $state "; + $Clipsal_CBus::Talker->set($command); + $Clipsal_CBus::Talker_last_sent = $command; + $Clipsal_CBus::Command_Counter = 0 + if ( + ++$Clipsal_CBus::Command_Counter > $Clipsal_CBus::Command_Counter_Max ); +} + +=item C + + Returns a hash of voice commands where the key is the voice command name and the + value is the perl code to run when the voice command name is called. + + Higher classes which inherit this object may add to this list of voice commands by + redefining this routine while inheriting this routine using the SUPER function. + + This routine is called by L to generate the + necessary voice commands. + +=cut + +sub get_voice_cmds { + my ($self) = @_; + my %voice_cmds = ( + + 'set on' => $self->get_object_name . '->set( 100 , "Voice Command")', + 'set off' => $self->get_object_name . '->set( 0 , "Voice Command")' + ); + + return \%voice_cmds; +} + +=head1 AUTHOR + + Jon Whitear, jonATwhitearDOTorg + +=head1 LICENSE + + This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty + of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program; if not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +=cut + +1; diff --git a/lib/Clipsal_CBus/Unit.pm b/lib/Clipsal_CBus/Unit.pm new file mode 100644 index 000000000..8a6309b77 --- /dev/null +++ b/lib/Clipsal_CBus/Unit.pm @@ -0,0 +1,67 @@ + +=head1 B + +=head2 SYNOPSIS + +Unit.pm - support for the Clipsal CBus input units, such as wall switches. + +=head2 DESCRIPTION + +This module is a child of Clispal_CBus, and simply allows for creation of +CBus input unit objects, such as wall switches. Unit names and addresses are +added to the $Clipsal_CBus::Units hash, so that when CGate receives a message +from CBus that an output group has been set, the name of the unit that set it +can be looked up and added to the log entry. + +=cut + +package Clipsal_CBus::Unit; + +use strict; +use Clipsal_CBus; + +@Clipsal_CBus::Unit::ISA = ( 'Generic_Item', 'Clipsal_CBus' ); + +=item C + + Instantiates a new object. + +=cut + +sub new { + my ( $class, $address, $name, $label ) = @_; + my $self = new Generic_Item(); + + &::print_log("[Clipsal CBus] New unit object $name at $address"); + + $self->set_label($label); + + #Add this object to the CBus object hash. + $Clipsal_CBus::Units{$address}{name} = $name; + $Clipsal_CBus::Units{$address}{label} = $label; + $Clipsal_CBus::Units{$address}{note} = "Added by object creation"; + + bless $self, $class; + + return $self; +} + +=head1 AUTHOR + + Jon Whitear, jonATwhitearDOTorg + +=head1 LICENSE + + This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty + of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program; if not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +=cut + +1; + diff --git a/lib/read_table_A.pl b/lib/read_table_A.pl index 226318cda..2e5ff25fe 100644 --- a/lib/read_table_A.pl +++ b/lib/read_table_A.pl @@ -64,6 +64,33 @@ sub read_table_A { $object = "ZWave_Appliance_Item('$address', $other)"; } + # -[ Clipsal CBus ]------------------------------------------------- + elsif ( $type eq "CBUS_CGATE" ) { + require Clipsal_CBus; + require Clipsal_CBus::CGate; + ( $name, $grouplist, @other ) = @item_info; + $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $object = "Clipsal_CBus::CGate('Clipsal_CBus_Cgate',$other)"; + } + elsif ( $type eq "CBUS_GROUP" ) { + require Clipsal_CBus::Group; + ( $address, $name, $grouplist, @other ) = @item_info; + $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $object = "Clipsal_CBus::Group('$address','$name',$other)"; + } + elsif ( $type eq "CBUS_TRIGGER" ) { + require Clipsal_CBus::TriggerGroup; + ( $address, $name, $grouplist, @other ) = @item_info; + $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $object = "Clipsal_CBus::TriggerGroup('$address','$name',$other)"; + } + elsif ( $type eq "CBUS_UNIT" ) { + require Clipsal_CBus::Unit; + ( $address, $name, $grouplist, @other ) = @item_info; + $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $object = "Clipsal_CBus::Unit('$address','$name',$other)"; + } + # -[ UPB ]---------------------------------------------------------- elsif ( $type eq "UPBPIM" ) { require 'UPBPIM.pm'; @@ -1373,13 +1400,14 @@ sub read_table_A { $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "AD2_Partition('$instance','$number','$address','$other')"; } - elsif($type eq "AD2_OUTPUT") { + elsif ( $type eq "AD2_OUTPUT" ) { require AD2; - my ($instance,$output); - ($name, $instance, $output, $grouplist, @other) = @item_info; - $other = join ', ', (map {"'$_'"} @other); # Quote data + my ( $instance, $output ); + ( $name, $instance, $output, $grouplist, @other ) = @item_info; + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "AD2_Output('$instance','$output','$other')"; } + #-------------- End AD2 Objects ------------- elsif ( $type =~ /PLCBUS_.*/ ) { require PLCBUS;